반응형

강의를 따라하며 실습을 진행하다가 강사님의 화면에서 IDE를 이용해 db ui를 확인하는 장면을 보았다.

관련해서 언급이 없어서 삽질을 좀 하고... 어찌저찌 연결하긴 했는데 관련 내용 검색을 했을때 정확한 내용을 찾지 못해 내 해결법을 공유한다.

 

[1] database 아이콘 좌측 네비게이션 바로 이동

 

지금 필요한 것은 IDE의 네비게이션 바에 database가 추가되는 것이다.

참고로 나는 좌측 네비게이션 바에 두었지만 좌측하단 혹은 우측에 두어도 상관은 없는듯!!

 

먼저 처음부터 database 아이콘이 보인다면 상관없겠지만 처음에 보이지 않는다면 다음을 따라한다.

 

1) 우측 메뉴툴바에서 View > Tool Windows확인

2) database 클릭

 

그러면 짜잔~ 하고 나타난다. 이제 db를 연결해 줄 차례이다!

 

[2] Laravel 프로젝트 내 databse.app 확인

Laravel은 앱을 생성하면 config나 다른 기타 설정 파일들이 함께 들어오더라! 약간 Django 처럼~~??

먼저 db 연결을 위해 어떤 정보가 있는지 확인해자

1) 연결할 DB종류 선택

나의 프로젝트는 mysql을 사용중이다! 따라서 Data Source > MySQL 을 선택해 준다.

 

2) 필요한 정보 확인

 

Host UserName Password Database URL Port 정보가 필요하다. 모두 database.php 파일에서 확인가능하다 ㅎㅎ

 

3) Config > database.php 확인

해당 파일을들어가면 아래와 같은 코드가 작성되어 있다.

.env 파일에서 값을 찾고 없을 경우 default값을 참조하도록 되어있는데 이때 env 파일에 있는 값도 있지만 없는 값도 있어서 잘 확인해 주어야 한다.!

 

해당 정보들에서 위에 입력에 필요한 정보들을 확인해 둔다.

[3] database 연결하기

이제 연결할 차례이다 참고로 이때 docker를 실행중에 있어야 한다!

Host 가 localhost 이기 때문~

 

참고로 정보를 입력한 뒤 아래의 Test Connection 을 누르면 Failed 뜬다.. 그치만 연결 된다 ㅋㅋㅋ

 

아까도 말했듯이 docker가 실행중이 아니라면 DB 연결이 실패한다!!! 아래와 같은 에러메세지가 뜨고 db 안은 텅 비어있다.

따라서 docker를 꼭 실행해 주도록 한다. 이때 Laravel 프로젝트로 실행하므로 나는 아래와 같은 명령어로 도커를 background에서 실행한다.

verdor/bin/sail up -d

 

맨날 vendor/bin/sail 입력하는게 귀찮아서 alias 로 등록했다 ㅋㅋ

// alias 등록하기
> vi ~/.zshrc

alias sail="vendor/bin/sail"

// 변경된 사항 적용하기
> source ~/.zshrc

 

그럼 아까 실행했던 명령어를 아래와 같이 입력할 수 있다.

 

Docker Desktop 켜주고.. 실행하기... ㅎㅎ...

프로젝트 하나 실행하는데 터미널을 무쟈게 많이 띄우긴 곤란하니 docker는 백그라운드에서 실행! 그리고 프론트 쪽 소스도 적용하기 위해서 npm run dev도 실행해 준다.

 

이렇게 실행하고 나면 아까 연결해 두었던 db를 새로고침!

데이터양마다 다른진 모르겠지만.. 1초도 안걸린다!

Refresh 를 하고 나면 그동안 프로젝트로 생성한 table과 그 안의 document들을 확인할 수 있다!

 

참고로 처음에 db를 연결할때 database 를 Laravel 로 하였기 때문에 해당 db가 연결된 것이다.

 

끝~~!!

반응형
반응형

코틀린 강의를 들으면서 한가지 기능에 대해 가장 쉽고, 빠르고, 코틀린만의 문법규칙을 사용하지 않고 작성하여 기능을 확인한 뒤 점진적으로 리팩토링 하며 코틀린 문법에 익숙해지려 한다.

 

강의를 들으면서 3단계에 걸쳐 동일한 기능을 리팩토링 하였고 그 과정을 기억해보려 한다.

[기능] 기능 설명

카테고리가 있는 책들의 현재 대출 현황에 대한 갯수를 보여준다.

{
    "COMPUTER": 1,
    "SCIENCE": 2,
    "SOCIATY": 5
}

카테고리 종류는 더 많지만 대출중인 유효 갯수가 있는 카테고리만 반환한다.

> bookRepository

현재 도서관이 보유중이 책 목록을 조회할 수 있다.

 

> BookStatResponse

카테고리별 대출중인 책의 갯수를 담는 DTO 이다.

 

[1] for loop 로 조회하기

- serivce

@Transactional(readOnly = true)
fun getBookStatistics_v1(): List<BookStatResponse> {
    val results = mutableListOf<BookStatResponse>()
    val books = bookRepository.findAll()
    for (book in books) {
        val targetDto = results.firstOrNull { dto -> book.type == dto.type }
        if (targetDto == null) {
            results.add(BookStatResponse(book.type, 1))
        } else {
            targetDto.plusOne()
        }
    }
    return results
}

 

- BookStatResponse

data class BookStatResponse(
        val type: BookType,
        var count: Int,
) {
    fun plusOne() {
        count++
    }
}

 

> 문제점

- 코드가 너무 길다

- BookStatResponse의 count가 가변 필드로 되어 의도치 않은 수정이 일어날 수 있다.

 

[2] ?. 와 ?: 사용하기

- service

@Transactional(readOnly = true)
fun getBookStatistics_v2(): List<BookStatResponse> {
    val results = mutableListOf<BookStatResponse>()
    val books = bookRepository.findAll()
    for (book in books) {
        results.firstOrNull { dto -> book.type == dto.type }?.plusOne()
                ?: results.add(BookStatResponse(book.type, 1))
    }
    return results
}

 

> 문제점

- call chain이 길어져 디버깅이 쉽지 않다.

- BookStatResponse의 count가 가변 필드로 되어 의도치 않은 수정이 일어날 수 있다.

 

[3] group by 사용하기

- service

@Transactional(readOnly = true)
fun getBookStatistics(): List<BookStatResponse> {
    return bookRepository.findAll() //List<Book>
            .groupBy{ book -> book.type }  // Map<BookType, List<Book>>
            .map { (type, books) -> BookStatResponse(type, books.size)} // List<BookStatResponse>
}

 

- BookStatResponse

data class BookStatResponse(
        val type: BookType,
        val count: Int,
)

 

> 코드 설명

1) findAll() 을 사용해 도서관에 등록되어 있는 모든 책을 조회한다 List<Book>

2) groupBy 를 사용해 카테고리 별 Map 으로 만들어준다 Map<BookType, List<Book>>

3) map 을 사용해 BookStatResponse DTO에 카테고리별 갯수를 담아준다 List<BookStatResponse

 

> 해결한 것

- BookStatResponse data class의 count 필드가 불변 필드로 되어 의도치 않은 수정을 막을 수 있다.

- 코드가 간결해 졌다.

 

> 문제점

도서관에 등록된 책의 전체 목록을 조회하여 그 갯수를 세는 과정이므로 db부하 등을 걱정해야 한다.

(전체 데이터 쿼리 => 메모리 로딩 + grouping)

 

[4] JPQL + Spring data JPA 활용하기 (예제 2개)

1] (위 예제와 이어지는 예제) 예제 1 : Book 기능

bookRepository에 커스텀 query function을 작성한다.

//jpql
@Query("select new com.group.libraryapp.dto.book.response.BookStatResponse(b.type, count(b.id)) " +
        "from Book b group by b.type")
fun getStats(): List<BookStatResponse>

 

위 메소드를 사용하면 service에서는 매우 간결해진다.

@Transactional(readOnly = true)
fun getBookStatistics(): List<BookStatResponse> = bookRepository.getStats()

 

해당 쿼리를 통해 조회하면 h2 query 출력은 아래와 같이 나온다

select
    book0_.type as col_0_0_,
    count(book0_.id) as col_1_0_ 
from
    books book0_ 
group by
    book0_.type

 

> 해결한 것

groupBy 쿼리를 사용하여 db로부터 전체 데이터를 메모리에 로드하지 않고 필요한 정보만을 가져오게 되었다.

-> index를 사용해 튜닝 (성능 최적화) 할수있는 여지가 잇다.

> 문제점

- query 구문에서 외부 객체를 사용하기 위해서는 절대 경로를 전부 입력해 주어야 한다...

- 오타와 띄어쓰기를 매우 주의해야 한다..

 

2] 예제 2 : User 기능

유저와 대출히스토리 table이 각각 있는 db구조에서 유저와 해당 유저의 전체 대출 히스토리 기록을 한번에 조회하는 기능이 있다. 이때 대출기록이 없는 유저일지라도 유저목록으로는 반환되야 한다. 

- service

@Transactional(readOnly = true)
fun getUserLoanHistories(): List<UserLoanHistoryResponse> {
    return userRepository.findAllWithHistories().map(UserLoanHistoryResponse::of)
}

 

- repository

@Query("select distinct u from User u left join fetch u.userLoanHistories")
fun findAllWithHistories(): List<User>

 

> 문제점

- 오타 문제

- 조건이 추가된다면 또 다른 jpql 문을 작성해야 한다.

- 어떤것을 참조하는지 타고 들어가 봐야 안다.

 

[5] Querydsl (예제 2개)

0] 코프링에 querydsl 적용하기

1) gradle 추가

먼저 코프링 환경에서 querydsl을 사용하기 위해서 gradle 에 추가해주어야 한다.

// plugin
id "org.jetbrains.kotlin.kapt" version "1.6.21"

// dependecy
implementation("com.querydsl:querydsl-jpa:5.0.0")
kapt("com.querydsl:querydsl-apt:5.0.0:jpa")
kapt("org.springframework.boot:spring-boot-configuration-processor")

참고로 환경은 스프링 부트 3버전 / intelliJ 23.1버전 / kotlin 1.16 버전 이다.

 

설정 변경후 빌드를 하면 build > generated > source > kapt > main > .....{프로젝트 루트} ..... > domain 에 User와 Book이 아닌 QUser QBook 처럼 앞에 Q가 붙은 클래스들이 생성된걸 볼 수 있다.

 

2) 스프링 빈 등록

 

그리고 나서 QuerydslConfig 파일을 만들어 Component로 등록하고 JPAQueryFactory를 Bean으로 생성한다.

import com.querydsl.jpa.impl.JPAQueryFactory
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import javax.persistence.EntityManager

@Configuration
class QuerydslConfig(
        private val em: EntityManager
) {

    @Bean
    fun querydsl(): JPAQueryFactory {
        return JPAQueryFactory(em)

    }
}

 

다음으로 querydsl을 사용할 interface와 구현체 클래스를 작성한다. 이는 기존의 repository 는 jpql + jpa를 사용하기 때문에 분리하기 위함이다.

 

3) querydsl 인터페이스, 구현체 생성

 

- interface : UserRepositoryCustom

interface UserRepositoryCustom { ... }

 

- class : UserRepositoryCustomImpl

class UserRepositoryCustomImpl(
        private val queryFactory: JPAQueryFactory
) : UserRepositoryCustom{ ... }

 

 

4) 기존 repository interface에 방금만든 것 상속

그리고 기존의 userRepository interface에서 Custom interface를 상속받도록 한다. 기존에 상속받고 있던 JPARepository는 그대로.

interface UserRepository : JpaRepository<User, Long>, UserRepositoryCustom{ ... }

 

1] 예제 2 : User 기능 => interface로 만들기

querydsl을 활용해 repository를 작성한다.

- UserRepositoryCustom

interface UserRepositoryCustom {
    fun findWithHistories(): List<User>
}

 

- UserRepositoryCustomImpl

import com.group.libraryapp.domain.user.QUser.user
import com.group.libraryapp.domain.user.loanhistory.QUserLoanHistory.userLoanHistory
import com.querydsl.jpa.impl.JPAQueryFactory

class UserRepositoryCustomImpl(
        private val queryFactory: JPAQueryFactory
) : UserRepositoryCustom{
    override fun findWithHistories(): List<User> {
        // querydsl 로 작성
        return queryFactory.select(user).distinct()
                .from(user)
                .leftJoin(userLoanHistory).on(userLoanHistory.user.id.eq(user.id)).fetchJoin()
                .fetch()
    }
}

 

의존성 주입을 위해 생성자에 JPAQueryFactory를 적어주어야 한다. query 구문에서는 기존 Entity class가 아닌 Q클래스를 사용한다.

 

2] (위 예제와 이어지는 예제) 예제 1 : Book 기능 => class로 만들기

BookQuerydslRepository 라는 클래스를 정의한다.

@Component
class BookQuerydslRepository(
        private val queryFactory: JPAQueryFactory,
) {
    fun getStats(): List<BookStatResponse> {
        return queryFactory.select(Projections.constructor(
                BookStatResponse::class.java,
                book.type,
                book.id.count()
        ))
                .from(book)
                .groupBy(book.type)
                .fetch()
    }
}

> Projections.constructor

주어진 DTO의 생성자를 호출한다. (첫번째로 나오는 클래스의 생성자)

그 뒤 인자들은 생성을 위한 초기값으로 사용된다.

select type, count(book.id) from book;

 

위 쿼리문을 해석하면 아래와 같다.

select type, count(book.id)
from book
group by type;

 

반응형
반응형

코프링 강의를 들으면서 프로젝트를 따라하다가 빌드를 하니
Downloading kotlinc-dist?

문구가 뜨면서 빌드가 끝나지 않았다. (5분 이상...)

 

이거저거 검색해 보니 최신버전 intelliJ에서는 kotin 1.6 버전을 지원하지 않는 문제일수 있다고 한 것을 보았다. (사실 정확한 해결책을 못찾았다. 한글은 없었다...)

1. kotlin 버전 업

command : 을 누르면 사용중인 언어의 버전을 확인할 수 있다.

해당 창에서 Language version을 누르면 현재 사용중인 버전과 선택 가능한 다른 이전 버전들을 확인할 수 있다.

> intelliJ 24.1 version의 화면

 

강의에서는 kotlin 1.6 버전을 사용중이었고, 소스를 그대로 다운받았기 때문에 내 프로젝트도 1.6 버전이다. 하지만 강의와 다른 점은 내 intelliJ의 버전이 24.1 버전이었고, 해당 버전에서는 Kotlin 1.6 버전을 더이상 지원하지 않는다는 것이다.

 

사실 강의 초반에는 문제될께 없었다. 정확하진 않지만 아래의 pluglin과 의존성을 추가한 뒤로 빌드 무한로딩 오류가 발생했다.

// plugin
id 'org.jetbrains.kotlin.plugin.jpa' version '1.6.21'


// dependecy
implementation "org.jetbrains.kotlin:kotlin-reflect:1.6.21"

 

command : 창에서 버전을 1.7로 올려주고 재 빌드했는데 자꾸 1.6으로 원복 되어 build.gradle 에서 버전을 올려주었다.

// plugin
id 'org.jetbrains.kotlin.jvm' version '1.7.21'
id 'org.jetbrains.kotlin.plugin.jpa' version '1.7.21'
// dependecy
implementation "org.jetbrains.kotlin:kotlin-reflect:1.7.21"

 

그러나 빌드 무한로딩 오류는 해결되지 않았다.

2. intelliJ 버전 다운그레이드

 

두번째 해결책으로는 kotlin 1.6 버전을 지원하지 않는 intelliJ 최신버전을 버리고 이전 버전으로 프로젝트를 실행하는 것이다.

jetBrain에서 intelliJ를 열고 "기타버전" 을 들어가 이전 버전을 다운로드 해준다.

 

그냥 젤 아래에 있는걸로 다운로드 하였다.

 

이후 다운그레이드 버전 intelliJ로 프로젝트를 실행하니 정상적으로 빌드가 완료되었다! (1분도 안걸림)

그리고 나서 command : 로 들어가 보니 kotlin 버전 1.6에서 deprecated 표시가 사라져 있었다.

> intelliJ 23.1 version의 화면

 

일단 이렇게 에러 해결 완료...

 

버전 문제를 여러번 맞이했지만 IDE와 언어의 버전 충돌은 생각치도 못했는데 🥲 어찌저찌 해결되어 다행이다!

반응형
반응형

코프링 강의를 듣는데 강의 h2 세팅이 인메모리로 되어있었다.

이것을 h2 실제 db로 세팅하고 서버를 재실행 해도 데이터를 보존하기 위해 일부 세팅을 바꿔주었다.

[1] h2 연결하기

1. h2 서버 연결

1) h2 실행하기

h2를 다운로드 한 후 파일 내에서 h2.sh 가 있는 곳으로 들어간다. 나의 경우 h2 > bin 에 있었다.

해당 파일이 있는 곳에서 파일을 실행시켜 준다.

./h2.sh

2) h2 db 접속하기

h2를 실행시키면 웹사이트가 열린다. db url을 입력해야 하는데 첫 연결시에는 해당 경로로 db가 생성된다.

나는 root경로 하위에 생성하도록 했다. 이후 연결 을 눌러 연결해 준다. 그러면 사이트 창이 하나 열리는데 화면 로딩이 안될 경우 해당 인터넷 창의 url 앞부분의 ip 주소 부분을 localhost로 변경해준다.

key 부분은 절대 수정하면 안된다!

http://localhost:8082/login.do?jsessionid=~~~~~

그렇게 되면 초기 화면을 볼 수 있다. 이후 재 연결시에는 tcp를 통해 연결해주도록 한다.

[2] h2연결을 위한 프로젝트 설정

1. application 설정 파일 수정 : yml 버전

먼저 application.yml에서 database의 설정을 변경해 준다.

기존에 프로젝트 소스코드에서는 인메모리 형식으로 지정되어 있었다.

spring:
  datasource:
    url: 'jdbc:h2:mem:library'
    username: 'user'
    password: ''
    driver-class-name: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        format_sql: true
        show_sql: true

간단하게 설명하자면

datasource에서 url이 h2 db의 접속 경로이다. :mem: 으로 되어있어 인메모리 db를 사용하는 것을 뜻한다.

jpa > hibername > ddl-auto 에는 크게 create와 none이 있다. create를 하면 서버를 재실행 할때마다 데이터를 리셋하면서 테이블 세팅을 다시 하는것이고, none이면 기존 설정을 유지하는 것이다.

만약 설정이 create 이고, 기존 테이블의 이름을 소스상에서 변경처리후 서버를 재실행 하면 기존 테이블 명으로도 남아있고, 새로운 테이블 명으로도 테이블이 생성된다.

 

위 설정값을 아래로 변경해 준다. tcp 통신을 통해 연결하므로 동시성 문제도 해결되는것으로 알고 있다.

spring:
  datasource:
    url: 'jdbc:h2:tcp://localhost/~/library'
    username: 'user'
    password: ''
    driver-class-name: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        format_sql: true
        show_sql: true
logging.level:
  org.hibernate.SQL: debug

db의 CRUD 쿼리를 보기 위해 log 설정을 추가해 주었다.

 

2. 스프링 서버 실행하기

위에 log 설정을 해두었기 때문에 프로젝트 실행 시 h2 db의 테이블 세팅 쿼리가 보여진다.

 

[3] USER 테이블 미생성 오류

bookuser_loan_history 라는 테이블은 클래스명 대로 잘 생성되었는데 user 테이블은 생성이 안되었다.

로그를 살펴보니 아래와 같은 에러메세지가 보인다.

Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "\000a    create table [*]user (\000a       id bigint generated by default as identity,\000a        age integer,\000a        name varchar(255) not null,\000a        primary key (id)\000a    )"; expected "identifier"; SQL statement:

    create table user (
       id bigint generated by default as identity,
        age integer,
        name varchar(255) not null,
        primary key (id)
    ) [42001-200]
	at org.h2.message.DbException.getJdbcSQLException(DbException.java:453) ~[h2-1.4.200.jar:1.4.200]

하지만 user 클래스는 @Entity 어노테이션도 잘 붙어있고, id 값에는 @GeneratedValue 로 제대로 세팅되어있는것으로 보인다.

@Entity
public class User {

  @Id
  @GeneratedValue(strategy = IDENTITY)
  private Long id;
}

 

> 예약어 : USER

이거저거 검색해보니 해당 클래스 명이 USER인데 이 USER라는 값은 mysql의 예약어 라고 한다. 때문에 개발자가 사용 불가능한 값이므로 해당 값을 테이블명으로 세팅할수 없다고 한다.

https://dev.mysql.com/doc/refman/8.0/en/keywords.html#keywords-in-current-series

 

클래스 명을 변경하기엔 다른 코드도 전부 수정해주어야 해서 안될것 같아 @Table 어노테이션을 사용하였다.

 

> @Table 어노테이션

해당 어노테이션의 parameter 값으로 name을 설정할 수 있다. 해당 값을 입력해두면 테이블 생성시 입력한 값으로 테이블 명이 지정된다.

예약어 이슈를 회피하기 위해 다음과 같이 설정해 주었다. 어노테이션을 붙여주면서 user 테이블의 이름을 복수형으로 지정했기 때문에 book 테이블 명도 복수형으로 지정해 주었다. 

@Entity
@Table(name="users")
public class User {
  @Id
  @GeneratedValue(strategy = IDENTITY)
  private Long id;
}

 

> 서버 재실행 후 테이블 생성 확인

이후 서버를 재 실행 해준다!

이때 ddl-auto 의 값은 create 여야 변경된 테이블 정보가 적용된다.

또한, 이전에 생성된 BOOK 테이블을 버리고 복수형으로 지정된 테이블을 생성하기 위해서 h2 에서 table drop 쿼리를 날려준다.

drop table if exists BOOK
drop table if exists USER_LOAN_HISTORY

그리고 서버를 실행해준다.

 

!! 테이블 명을 @Table 어노테이션으로 "소문자" 로 적어도 테이블 생성시 대문자로 생성된다!!

 

성공적으로 테이블이 생성되었다!

이후 application.yml 파일에서 ddl-auto 설정을 none 으로 해주어 서버 재 실행시에도 데이터가 날아가지 않도록 해준다!

반응형
반응형

[1] Class : null 객체로 선언하기 

1. class 만들기

먼저 코틀린에서 간단한 class를 선언한다.

class School {
    var name: String = ""
    var grade: Int? = null

    constructor(name: String) {
        this.name = name
    }
}

 

생성자는 일단 name 필드만 필요로 하는 것으로 만들어 두었다.

grade 필드는 null safe 필드로 선언하여 Int타입 뒤에 ? 를 붙여서 선언하였다.

 

참고로 kotlin 에서는 class를 선언할때 모든 필드의 초기값을 세팅해 주어야 한다.

 

2. School 인스턴스 만들기

1) null safe 객체가 아닌데 null 로 초기화

// X 컴파일 오류 : null safe 하지 않은 타입인데 null로 초기화 할 수 없다
var school2: School = null
// X 컴파일 오류 : constructor 에 선언된 필수값을 입력해야 한다.
var school2: School = School()
// O 인스턴스 생성 성공
var school2: School = School("유치원")

null-safe 하게 선언하지 않았기 때문에 인스턴스의 초기값으로 null을 세팅할 수 없다.

또한 School 클래스의 constructor 로 name을 필수로 입력받도록 했기 때문에 인스턴스 초기화 시 해당 값을 필수로 입력해 주어야 한다.

위 예제에서는 컴파일 오류가 발생하기 때문에 쉽게 에러를 찾을 수 있다.

 

2) null safe 객체로 만들기

> null 로 초기화 후 인스턴스의 필드 가져오기 시도 -> 컴파일 오류 발생

// null safe 객체로 표현한 school1
var school1: School? = null 
// 컴파일 에러 발생 : null 객체에서 가져올 수 없다
var school1nameNo = school1.name

인스턴스가 null 이기 때문에 null.{필드} 는 Java와 마찬가지로 NullPointExceptioin 이 발생한다. Java와 다른점은 NullPointException이라고 뜨지 않고 

 

e: file://~/TryCatch.kt:20:32 Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type School?

 

에러메세지가 표기되는데 읽어보면 null 관련 메세지임을 알 수 있다. 마찬가지로 컴파일 오류로 쉽게 에러를 확인할 수 있다.

 

> null로 초기화 후 실체 생성

// null safe 객체로 표현한 school1
var school1: School? = null 
println("--------------- school ------------------")

school1 = School("학교") // 생성자로 실체 인스턴스 생성
var school1name = school1.name // 정상 실행
println(school1name)
var school1Grade = school1.grade // 정상 실행 : null 로 나온다
println(school1Grade)

>> "학교"
>> null

 

name을 필수값으로 받는 생성자를 통해 shcool1을 선언하고, name필드와 null-safe한 grade 필드를 출력하면 에러 발생없이 그대로 출력된다. 이때 null 로 초기화 되어 값이 선언되지 않은 grade는 그대로 null로 출력된다.

 

3) 다수의 constructor + null-safe 필드

grade 값도 입력으로 받는 또하나의 constructor를 생성한다.

class School {
    var name: String = ""
    var grade: Int? = null

    constructor(name: String) {
        this.name = name
    }

    constructor(name: String, grade: Int?) {
        this.name = name
        this.grade = grade
    }
}

이때 grade 필드는 null-safe 필드이다!

 

이후 해당 constructor를 통해 인스턴스를 생성하면 다음과 같다. 정확한 확인을 위해 name 필드만을 받는 constructor는 삭제했다.

var school3: School = null // 컴파일 오류 : null safe 하지 않은 타입인데 null로 초기화 할 수 없다
var school3: School = School() // 컴파일 오류 : constructor 에 선언된 필수값을 입력해야 한다.
var school3: School = School("유치원")  // 컴파일 오류 : constructor에 선언된 모든 필드를 입력해야 한다.
var school3: School = School("대학교", null) // 성공
var school4: School = School("대학원", 1) // 성공

 

null-safe한 필드여도 constructor에 선언되어 있으면 인스턴스 생성시 필수로 입력해야 한다. 단, null로 입력이 가능할 뿐이다.

 

[1] Var : null safe 변수

1. null safe 변수

변수에서도 null-safe 변수를 생성할 수 있다. 위 class 예제를 이해하면 변수도 똑같은 메커니즘으로 동작하는것을 이해할 수 있다.

 

1) null-safe 변수 선언 후 값을 대입하여 확인하기

var name: String? = null // 된다. null을 대입할수 있고, 대이하려면 타입 뒤에 ? 를 붙인다.
name = "jello" // null로 대입된 변수는 이후 다른 값으로 변경될 수 잇다.
println(name)
println(name[0])

>> jello
>> j

 

null-safe 변수의 값을 null로 초기화 한 후 값을 대입하여 사용하면 된다.

2) null-safe 하지않은 타입에 null로 초기화시 컴파일 오류 발생

var nameNull: String = null

다음과 같은 컴파일 오류가 발생한다.

Null can not be a value of a non-null type String

 

3) null-safe 변수값 세팅하지 않고 문자열 더하기

var name0: String? = null
var aa = name0 + "1"
println(aa)
>> null1
println(name0[0]) // complie 오류로 잡힌다.

null 상태인 변수값에 "1" string값을 더해 print 하면 컴파일 오류, 런타임 오류 없이 "null1" 로 출력된다.

단, null 상태인 String 타입의 변수를 위에 유효한 값을 대입했던것과 같이 index로 찾으려 하면 컴파일 오류가 발생한다.

Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?

에러메세지는 위 class 예제에서 null로 초기화된 인스턴스의 필드에 접근하려 할때 발생했던 에러메세지와 동일하다.

 

2. non-nullable 변수에 null-safe 변수값 대입

캐스팅만 해주면 변수간 대입이 가능하다. 서로 다른 타입도 캐스팅으로 가능하지만 non-nullabler과 nullable 사이에서도 가능하다. 이때 캐스팅 하는 방법은 여러가지가 있다.

1) null 체크 후 캐스팅

null-safe한 변수가 현재 기준으로 null 상태인지 확인하고, null 이 아닐때에만 non-nullable한 동일타입 변수에 대입한다.

var name1: String? = null
var name2: String = ""

// name 이 현재 null 인지 체크
if (name != null) {
    name2 = name
}

단, 현재 kotlin.spring 설정으로 smart casting이 이뤄지고 있다. null 체크를 자동으로 하여 변환해주고 있는 듯 하다.

name2 = name
// 원래는 안되지만 kotlin.Spring 에서 smart casting 이 되어 자동으로 null 체크가 이뤄지는 듯

2) !!를 붙여 ? 를 강제로 떼어내기

단, 이 방법은 개발자가 임의로 null 이 아니라고 한것이기 때문에 컴파일 오류가 아닌 런타임에러가 난다.

name2 = name!!

 

3) Safe call : ?. 과 let 으로 조건확인하여 대입하기

?. 은 null 여부를 체크하고 null 이 아니라면 그 뒤 명령어를 실행한다.

이런식의 null check 는 safe call 을 위한 방법으로 코틀린 공식문서에서도 소개되고 있다.

 

name?.let{
    name2=name
}

확인하여 name의 값이 null 이 아니면 name2에 값을 대입하도록 한다.

반응형
반응형

개발의 빠름빠름을 위해 나만의 커스텀 단축키를 생성해보자.

1. Preferences > Live Template 검색

 

2. + 클릭하고 Live Template 클릭

이때 기존에 생성되어있는 목록을 누르고 생성하게 되면 해당 카테고리의 하위항목으로 등록된다.

따라서 빈 곳을 클릭 하여 활성화된 기존 목록이 없게 끔 하고 나서 생성을 진행한다.

 

3. 나만의 템플릿 생성하기

Abbreviation에 해당 템플릿을 빠르게 불러올 나만의 단축키를 등록한다. Description은 선택사항!

김영한님의 스프링 강의를 들으면서 테스트코드 템플릿을 자주 접하게 되었는데 이 템플릿을 나도 등록하여 사용하면 실무에서도 유용하게 사용하게 될것 같아서 등록~!

 

테스트명은 작성할때마다 지정하는것이니 해당부분은 빼놓고, 기본 Exception 처리만 추가하여 생성한다.

 

4. 작동 언어 세팅

아마 처음 등록하는 거면 표기되는 문자가 달랐던것 같지만.... 하단의 Change 를 클릭하여 해당 템플릿이 작동할 언어를 선택해 준다.

Java 하면 됨~!

 

5. 사용해보기

tdd를 치면 test template 이라고 내가 설정한 Description이 함께 뜬다! 바로 엔터를 눌러준다

짜잔~

완성이닷

반응형
반응형

-- [문법] -----------

1. 값 전달

자바에서 대입은 항상 변수에 들어있는 값을 복사해서 전달한다.

 

만약 변수가 "배열" 이면 해당 배열에는 참조값이 들어있다. 따라서 이 참조값이 전달되는 것이다.

배열 A = 'X001' 일때 A 값을 새로운 인자로 전달하게 되면 A의 value가 아닌 A의 참조값인 'X001' 이 전달된다. 따라서 전달받은 곳에서 newA의 값을 변경하게 되면 동일한 참조값을 갖고 있는 A의 값도 변경되는 것이다.

-> 전형적인 call By Reference, call By Value 와는 조금 다른 양상

 

2. 클래스 배열 사용하기

#1 : 기본

fruit1 = new Fruit();
fruit1.name = "apple";

fruit2 = new Fruit();
fruit2.name = "orange";

Fruit[] fruits = new Fruit[2];
fruits[0] = fruit1;
fruits[1] = fruit2;

 

#2 : 클래스 배열 선언과 초기화 동시에 진행

fruit1 = new Fruit();
fruit1.name = "apple";

fruit2 = new Fruit();
fruit2.name = "orange";

Fruit[] fruits = new Fruit[2]{fruit1, fruit2};

 

#3: 클래스 배열 선언 간결화

fruit1 = new Fruit();
fruit1.name = "apple";

fruit2 = new Fruit();
fruit2.name = "orange";

Fruit[] fruits = new Fruit[]{fruit1, fruit2};
// 혹은 아래와 같이
Fruit[] fruits = {fruit1, fruit2};

 

3. 기본형과 참조형

기본형 : 자바에서 기본으로 제공하는 데이터 타입 : int, long, dubloe, boolean... 소문자로 시작한다.

참조형 : 기본형을 제외한 타입으로 대문자로 시작한다. 클래스 역시 대문자로 시작하고, 참조형이다. 개발자는 참조형인 클래스만 직접 정의할 수 있다.

- 참조형 String

String은 사실 "클래스" 이다. 하지만 기본형 처럼 문자 값을 바로 대입할수 있으며 서로 더할수 있다. 이는 자주다뤄지는 타입이기 때문에 자바에서 특별한 편의기능을 제공하는 덕분이다.

 


 

-- [객체지향] -------

1. 생성자

> 생성자 규칙

- 생성자의 이름은 클래스 이름과 같아야 한다. (따라서 첫 글자도 대문자로 시작한다.)

- 생성자는 반환 타입이 없다. 비워두어야 한다.
- 나머지는 메서드와 같다.

 

> 기본 생성자

매개변수가 없는 생성자를 기본 생성자라고 한다.

클래스에 생성자가 하나도 없으면 자바 컴바일러는 매개변수가 없고, 작동하는 코드가 없는 기본 생성자를 자동으로 만들어 준다.

public class Fruit {
	String name;
    int price;
    // 자바 컴파일러가 자동으로 만들어 주는 기본 생성자
    Fruit(){}
}

하지만, 생성자가 하나라도 있으면 자바는 기본 생성자를 만들지 않는다.

public class Fruit {
	String name;
    int price;
    // 사용자가 만든 생성자
    void Fruit(String name){
    	this.name = name
    }
}

> 생성자 오버로딩

매개변수가 서로 다른 생성자를 다수 만들 수 있다.

public class Fruit {
	String name;
    int price;
    // name만 받는 생성자
    void Fruit(String name){
    	this.name = name
    }
    
    // name과 price를 받는 생성자
    void Fruit(String name, int price){
        this.name = name
        this.price = price
    }
}

// 두가지 생성자를 활용한 객체 만들기
Fruit oneFruit = new Fruit("사과"); // 이때 price는 int형의 초기값인 0이 된다.
Fruit twoFruit = new Fruit("오렌지", 1000);

두개 생성자의 공통된 코드를 관리할 수 있다. this() 를 통해 생성자 내에서 객체 자기자신의 생성자를 재 호출 한다.

이때 this()는 생성자 코드의 첫번째 줄에서만 사용 가능하다!

public class Fruit {
	String name;
    int price;
    // 생성자 1 : name만 받는 생성자
    void Fruit(String name){
    	this(name, 0) // 매개변수에 따라 생성자 2가 호출된다.
    }
    
    // 생성자 2 : name과 price를 받는 생성자
    void Fruit(String name, int price){
        this.name = name
        this.price = price
    }
}

 


-- [메모리 구조와 static]

1. Java의 메모리 영역

- 메소드 영역 : 클래스 정보 보관 (붕어빵 틀)

- 스택 영역 : 실제 프로그램이 실행되는 영역. 메소드 실행할때 마다 하나씩 쌓인다 (붕어빵 만드는 로직)

- 힙 영역 : 객체 (인스턴스) 가 생성되는 영역. new 명령어로 새 객체를 생성하면 해당 영역이 사용된다 (만들어진 붕어빵들)

> 메소드 영역 : 프로그램 실행에 필요한 공통 데이터 관리

- 클래스 정보 : 클래스의 실행 코드 (바이트코드), 필드, 메소드와 생성자 코드 등 모든 실행 코드 존재

- static 영역 : static 변수 보관

- 런타임 상수 풀

> 스택 영역 : 자바 실행 시 하나의 실행 스택 생성

- 지역변수, 중간 연산결과, 메소드 호출 정보 등 포함

- 쓰레드별 하나씩 생성되는 영역

> 힙 영역

- 객체(인스턴스)와 배열이 생성되는 영역

- GC가 일어나는 영역

- 객체가 생성되어 힙 영역이 사용되어도 인스턴스의 메소드는 힙 영역을 사용하지 않고 메소드 영역을 사용한다.

 

2. Static

> 변수와 생명주기

  • 지역 변수(매개변수 포함) : 지역 변수는 스택 영역에 있는 스택 프레임 안에 보관된다. 메서드가 종료되면 스택 프 레임도 제거 되는데 이때 해당 스택 프레임에 포함된 지역 변수도 함께 제거된다. 따라서 지역 변수는 생존 주기가 짧다.
  • 인스턴스 변수 : 인스턴스에 있는 멤버 변수를 인스턴스 변수라 한다. 인스턴스 변수는 힙 영역을 사용한다. 힙 영 역은 GC(가비지 컬렉션)가 발생하기 전까지는 생존하기 때문에 보통 지역 변수보다 생존 주기가 길다.
  • 클래스 변수 : 클래스 변수는 메서드 영역의 static 영역에 보관되는 변수이다. 메서드 영역은 프로그램 전체에서 사용하는 공용 공간이다. 클래스 변수는 해당 클래스가 JVM에 로딩 되는 순간 생성된다. 그리고 JVM이 종료될 때 까지 생명주기가 이어진다. 따라서 가장 긴 생명주기를 가진다.

 


-- [Final] -----

값이 최초 한번만 할당되고 이후에 변경 불가능함

1. final 변수의 종류

> final 지역 변수

public static void main(String[] args) {
	// final 변수 1
    public final String value1;
    value1 = "r";
    value2 = "b"; // 컴파일 오류
    
    // final 변수 2
    public final String value2="a";
    value2 = "b"; // 컴파일 오류   
}

 

> final 멤버 변수

- 생성자로 초기화

//final 필드 - 생성자 초기화
public class ConstructInit {
     final int value;
     // 생성자에서 값을 초기화
     public ConstructInit(int value) {
         this.value = value;
} }

- 필드 초기화

//final 필드 - 필드 초기화
public class FieldInit {
     static final int CONST_VALUE = 10;
     final int value = 10;
 }

 

? 필드 초기화에서 final 멤버변수에 static을 사용하는 이유

-> 모든 인스턴스가 같은 값을 갖고있는데 중복된 값을 모두 메모리 사용하여 갖고 있다. 이는 명확한 중복이며 개선의 여지가 있는 부분으로 보인다. 따라서 이러한 값은 static을 붙여서 메모리 사용률을 줄이는 것이 효과적이다.

 

2. final 참조형 변수의 값 변경

// final 로 생성된 참조형 변수
final Data data = new Data()

 

위와 같은 참조형 변수가 final로 선언되었을때 변수 data를 다른 객체로 재 정의할 순 없다. 하지만 참조형 변수인 Data 인스턴스의 인스턴스 필드는 변경할 수 있다.

// final로 선언된 참조형 변수 Data 객체 data
final Data data = new Data();

data = new Data(); // final 변경 불가 컴파일 오류
data.value = 10; // 가능
data.value = 20; // 가능

 


 

반응형
반응형

[프로젝트 생성]

1. java, jdk, spring boot 버전 확인

2. 초기라이브러리 

- spring-web

- thymleaf

- lombok

 

[MTV pattern]

1. 의존성

1) @Component 와 @Autowired

스프링 bean 으로 등록하기 위해선 @Component 어노테이션을 달아주어야 한다. 만약 해당 객체 (Java는 모든것이 객체) 의 생성자를 만들때 의존성 주입이 필요하다면 @Autowired로 빌드 시 자동으로 주입되도록 한다.

생성자를 만드는 규칙은 3가지 이다

• setter 주입 (수정자 주입)
• field 주입
• 일반 메소드 주입

• 생성자 주입

권장되는 방법은 이중 "생성자 주입" 방식이다. 만약 생성자가 단 1개라면 @Autowired의 생략이 가능하다.

@Controller
@RequestMapping("/basic/items")
public class BasicItemController {
    private final ItemRepository itemRepository;

    @Autowired // 생성자가 단 한개이므로 생략 가능하다
    public BasicItemController(ItemRepository itemRepository) {
        this.itemRepository = itemRepository;
    }
}

 

이때 required = False 처리를 하게 되면 주입 대상이 없을 경우 컴파일 에러가 발생하지 않지만 실제 주입 대상이 존재하지 않으면 호출되지 않는다.

@Controller
@RequestMapping("/basic/items")
public class BasicItemController {
    private final ItemRepository itemRepository;

    @Autowired(required=false)
    public BasicItemController(ItemRepository itemRepository) {
        this.itemRepository = itemRepository;
    }
}

 

2. 생성자 

1) @RequiredArgsConstructor

Lombok의 어노테이션을 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.

이때 해당 객체는 생성자 주입시 필수 객체로 인지된다.

@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor
public class BasicItemController {
    private final ItemRepository itemRepository;
}

 

2) @AllArgsConstructor

모든 값을 필요로 하는 생성자를 만들어준다.

 

@Data
@AllArgsConstructor // 해당 객체를 만들려면 모든 값을 넘겨야 한다는 뜻
static class UpdateMemberResponse {
    private Long id;
    private String name;
}

위 객체를 만들기 위해서는 아래와 같이 생성해야 한다.

UpdateMemberResponse res = new UpdateMemberResponse(findMember.getId(), findMember.getName());

 

3) @NoArgsConstructor

기본생성자를 생성한다.

3. Controller

1) @Controller

스프링 빈으로 등록하기 위해 Component로 등록해야 하지만 Controller 어노테이션에 Component 어노테이션이 부착되어 있으므로 Contoller로 등록한다.

2) @RestController

@Controller 는 응답값이 html 파일로 전송되게 한다. @ResponseBody를 쓰면 되긴 하지만...

일반적으로 rest api 개발시 해당 동작은 불필요 하므로 간편하게 @RestController를 사용한다. (아래 예시)

- Controller사용과 메소드에 적용

@Controller
public class RequestBodyJsonController {
    private ObjectMapper objectMapper = new ObjectMapper();

    @ResponseBody
    @PostMapping("/hello")
    public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {
        HelloData data = objectMapper.readValue(messageBody, HelloData.class);
        log.info("username={}, age={}", data.getUsername(), data.getAge());
        return "ok";
    }

 

- RestController 사용

@RestController
public class RequestBodyJsonController {
    private ObjectMapper objectMapper = new ObjectMapper();

    @PostMapping("/hello")
    public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {
        HelloData data = objectMapper.readValue(messageBody, HelloData.class);
        log.info("username={}, age={}", data.getUsername(), data.getAge());
        return "ok";
    }

 

3) @RequestBody

HttpMessageConverter 사용 -> StringHttpMessageConverter 적용

개별 메소드에 적용할 수 있고, RestController를 사용함으로써 생략할 수도 있다.

 

4) @ResponseBody

- 모든 메서드에 @ResponseBody 적용
- 메시지 바디 정보 직접 반환(view 조회X)
- HttpMessageConverter 사용 -> StringHttpMessageConverter 적용

 

 

[Bean 등록]

1) @Configuration

직접 스프링 빈을 등록하기 위해서는 클래스를 구성정보/설정정보로 나타내주고 하위에 등록한다.

@Configuration // 구성정보, 설정정보
public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }
}

위 예제에서 memberService를 스프링 빈으로 등록하려는데 memberRepository에 대한 의존성을 갖고있다. 따라서 이것도 스프링 빈으로 등록해 주어야 한다.

@Configuration // 구성정보, 설정정보
public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }
    
    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}

 

[Validation]

값에 대한 검증은 필수이다. 모든걸 비즈니스 로직으로 처리하면 공수가 크고 유지보수가 어려우므로 스프링에서 제공하는 @Validated나 자바 표준 모듈인 @Valid를 사용한다. 둘중 어느걸 사용해도 상관 없다고 한다. 스프링 이라는 틀은 변하지 않을 것 같으므로 @Validated를 사용한다.

 

1) 직접 검증기 생성하기 (Validator 구현체 생성)

public interface Validator {
     boolean supports(Class<?> clazz);
     void validate(Object target, Errors errors);
}

위 인터페이스를 구현하는 나만의 Validator를 구현한다.

package hello.itemservice.web.validation;

import hello.itemservice.domain.item.Item;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

@Component
public class ItemValidator implements Validator {
    @Override
    public boolean supports(Class<?> clazz) {
        return Item.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        Item item = (Item) target;
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "itemName", "required");
        if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
            errors.rejectValue("price", "range", new Object[]{1000, 1000000}, null);
        }
        if (item.getQuantity() == null || item.getQuantity() > 10000) {
            errors.rejectValue("quantity", "max", new Object[]{9999}, null);
        }
        //특정 필드 예외가 아닌 전체 예외
        if (item.getPrice() != null && item.getQuantity() != null) {
        int resultPrice = item.getPrice() * item.getQuantity();
        if (resultPrice < 10000) {
            errors.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
        }
    }
}

 

- 검증기 등록하기

@Component
 public class ItemValidator implements Validator {
     @Override
     public boolean supports(Class<?> clazz) {
         return Item.class.isAssignableFrom(clazz);
     }
    @Override
     public void validate(Object target, Errors errors) {
     }
 }

 

#1 특정 컨트롤러에 적용하기

// 적용할 클래스 안에서
@InitBinder
 public void init(WebDataBinder dataBinder) {
     log.info("init binder {}", dataBinder);
     dataBinder.addValidators(itemValidator);
 }

#2 모든 컨트롤러에 적용하기

@SpringBootApplication
 public class ItemServiceApplication implements WebMvcConfigurer {
     public static void main(String[] args) {
         SpringApplication.run(ItemServiceApplication.class, args);
}
     @Override
     public Validator getValidator() {
         return new ItemValidator();
     }
}

 

위 방법을 사용하게 될 경우 클래스 내의 InitBinder 어노테이션을 제거하여도 검증기로 등록되어 support 하는 클래스의 Validatior를 실행한다.

 

하지만 2번처럼 글로벌 설정을 하면 아래의 BeanValidator가 자동 등록되지 않고, 실제로 글로벌 설정을 직접 사용하는 경우는 드물다.

 

2) Bean Validation

해당 기능을 사용하기 위해선 의존관계 추가가 필요하다

 implementation 'org.springframework.boot:spring-boot-starter-validation'

  

Bean Validation을 구현한 기술중에 일반적으로 사용하는 구현체는 하이버네이트 Validator이다. 

Jakarta Bean Validation

- jakarta.validation-api : Bean Validation 인터페이스

- hibernate-validator : 구현체

 

package hello.itemservice.domain.item;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
public class Item {
    private Long id;

    @NotBlank(message = "공백은 입력할 수 없습니다.")
    private String itemName;

    @NotNull
    @Range(min = 1000, max = 1000000)
    private Integer price;

    @NotNull
    @Max(9999)
    private Integer quantity;

    public Item() {}
    public Item(String itemName, Integer price, Integer quantity) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
    }
}

 

검증 애노테이션

- @NotBlank : 빈값 + 공백만 있는 경우를 허용하지 않는다.
- @NotNull : null 을 허용하지 않는다.
- @Range(min = 1000, max = 1000000) : 범위 안의 값이어야 한다.

- @Max(9999) : 최대 9999까지만 허용한다.

 

 

- @Valid

javax.validation.@Valid 를 사용하려면 build.gradle` 의존관계 추가가 필요다.

implementation 'org.springframework.boot:spring-boot-starter-validation'

 

- @Validated

@Validated 는 검증기를 실행하라는 애노테이션이다.
이 애노테이션이 붙으면 앞서
WebDataBinder 에 등록한 검증기를 찾아서 실행한다. 그런데 여러 검증기를 등록한다 면 그 중에 어떤 검증기가 실행되어야 할지 구분이 필요하다. 이때 supports() 가 사용된다. 여기서는 supports(Item.class) 호출되고, 결과가 true 이므로 ItemValidator 의 validate()` 가 호출된다.

 

@PostMapping("/add")
    public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
        if (bindingResult.hasErrors()) {
            log.info("errors={}", bindingResult);
            return "validation/v3/addForm";
        }
        //성공 로직
        Item savedItem = itemRepository.save(item); redirectAttributes.addAttribute("itemId", savedItem.getId()); redirectAttributes.addAttribute("status", true);
        return "redirect:/validation/v3/items/{itemId}";
}

 

이때 @ModelAttribute에서도 바인딩에 대한 검증이 이뤄지는데 @ModelAttribute각각의 필드 타입 변환을 시도하고, 변환에성공한필드만BeanValidation적용된다.

 

- @ModelAttribute vs @RequestBody

- @ModelAttribute 는 필드 단위로 정교하게 바인딩이 적용된다. 특정 필드가 바인딩 되지 않아도 나머지 필드 는 정상 바인딩 되고, Validator를 사용한 검증도 적용할 수 있다.
- @RequestBody 는 HttpMessageConverter 단계에서 JSON 데이터를 객체로 변경하지 못하면 이후 단계 자 체가 진행되지 않고 예외가 발생한다. 컨트롤러도 호출되지 않고, Validator도 적용할 수 없다.

 

[Filter]

1) Filter의 흐름

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러

 

2) Filter의 생성

package hello.login.web.filter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;

public class LogFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("log filter init");
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
    FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String requestURI = httpRequest.getRequestURI();
        String uuid = UUID.randomUUID().toString();
        try {
             log.info("REQUEST  [{}][{}]", uuid, requestURI);
             chain.doFilter(request, response);
         } catch (Exception e) {
             throw e;
         } finally {
             log.info("RESPONSE [{}][{}]", uuid, requestURI);
}
        }
}

Filter 인터페스를 구체화하여 나만의 Filter를 생성한다. 이때 Filter 인터페이스의 init과 doFilter 메소드를 오버라이딩 하여 로직을 구현한다.

 

주의해야 할 점은 doFilter 로직 안에서 필터 로직을 진행 한후 doFilter를 호출해야 한다는 것이다. 이때 다음 필터가 있다면 그 필터가 실행되고, 필터가 없다면 서블릿을 호출한다. 

chain.doFilter(request, response);

 

 

3) Filter 등록

만든 Filter를 스프링이 인식하여 실제 동작하게 하기 위해선 Config에 등록해야 한다.

package hello.login;
import hello.login.web.filter.LogFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
@Configuration
public class WebConfig {
    @Bean
    public FilterRegistrationBean logFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new LogFilter());
        filterRegistrationBean.setOrder(1);
        filterRegistrationBean.addUrlPatterns("/*");
        return filterRegistrationBean;
    }
}

Filter를 등록할때에는 FilterRegistrationBean 클래스를 사용한다.

setOrder를 통해 실행될 필터의 우선순위를 설정하고, addUrlPatterns를 통해 어떤 경로 호출시 필터를 실행시킬지 결정한다.

4) Filter 특징

chain.doFilter를 통해 request와 response를 넘길때 해당 객체의 타입을 조작할 수 있다. Filter인터페이스의 기본 형은 Servlet~ 으로 사용할수 있는 메소드가 한정적이어서 doFilter 메소드 내에서 다운그레이드 하여 Http 객체로 변경처리하였다. 변경된 이 상태를 넘길 수 있다.

 

5) Filter와 에러처리

1. WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
2. WAS `/error-page/500` 다시 요청 -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러(/error-page/500) -> View

 

스프링MVC에서 오류 발생시 WAS로 에러가 전달되고, 해당 에러를 처리하기 위해 다시 Filter -> MVC의 흐름이 진행된다. 이럴때 정상 호출시에만 Filter만 호출되게 하고, 에러처리로 인한 실행에는 필터가 동작하지 않게 명시적으로 지정 할 수 있다.

@Bean
 public FilterRegistrationBean logFilter() {
     FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
     filterRegistrationBean.setFilter(new LogFilter());
     filterRegistrationBean.setOrder(1);
     filterRegistrationBean.addUrlPatterns("/*");
     filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
     return filterRegistrationBean;
}

Filter를 Bean으로 등록시 dispatcherType을 명시하지 않으면 default 값으로 REQUEST가 적용된다.

> DispatcherType 종류

- REQUEST : 클라이언트 요청
- ERROR : 오류 요청
- FORWARD : MVC에서 배웠던 서블릿에서 다른 서블릿이나 JSP를 호출할 때 RequestDispatcher.forward(request, response)
- INCLUDE : 서블릿에서 다른 서블릿이나 JSP의 결과를 포함할 때 RequestDispatcher.include(request, response)
- ASYNC : 서블릿 비동기 호출

 

[Interceptor]

1) 인터셉터의 흐름

HTTP 요청 ->WAS-> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러

 

2) 인터셉터의 생성

public interface HandlerInterceptor {
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    }

    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
    }

    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
    }
}

 

인터셉트를 구현하기 위해선 HandlerInterceptor 인터페이스를 구체화 한다.

 

- preHandle

: 컨트롤러 호출 전에 호출된다. (더 정확히는 핸들러 어댑터 호출 전에 호출된다.) 응답값이 "true" 이면 다음으로 진행하고, "false" 이면 더는 진행하지 않는다. "false" 인 경우 나머지 인터셉터는 물론이고, 핸들러 어댑터도 호출되지 않는다. 그림에서 1번에서 끝이 나버린다.

- postHandle

: 컨트롤러 호출 후에 호출된다. (더 정확히는 핸들러 어댑터 호출 후에 호출된다.) 이떄 컨트롤러 내부에서 에러가 발생했을 경우에는 호출되지 않는다.

- afterCompletion

: 컨트롤러의 에러 여부에 상관없이 뷰가 렌더링 된 이후에 호출된다.

 

3) 인터셉터의 등록

만든 인터셉터를 실행시키기 위해선 Config로 등록해야 한다. "WebMvcConfigurer" 가 제공하는 addInterceptors() 를 사용해서 인터셉터를 등록할 수 있다.

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/css/**", "/*.ico", "/error");
}
}

 

- order() : 인터셉터의 호출 순서를 지정한다. 낮을 수록 먼저 호출된다.
- addPathPatterns() : 인터셉터를 적용할 URL 패턴을 지정한다.

- excludePathPatterns() : 인터셉터에서 제외할 패턴을 지정한다.

 

 

[ArgumentResolver]

Filter와 Interceptor를 통해서도 인증을 체크할수 있지만 어노테이션을 활용하면 더욱 집약적으로 원하는 메소드에만 적용 할수 있다. 이때 ArgumentResolver를 활용한다.

인증된 유저만 접근할수 있는 컨트롤러를 예시로 들면 아래와 같다.

1) 사용 예시 : @Login

 @GetMapping("/")
 public String homeLoginV3ArgumentResolver(@Login Member loginMember, Model model) { //세션에 회원 데이터가 없으면 home
     if (loginMember == null) {
         return "home";
     }
     //세션이 유지되면 로그인으로 이동
     model.addAttribute("member", loginMember); return "loginHome";
}

 

위에 적용된 @Login 어노테이션이 정상적으로 동작할 수있도록 구현하면 아래와 같다.

2) 어노테이션 구현

package hello.login.web.argumentresolver;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {}

- @Target(ElementType.PARAMETER) : 파라미터에만 사용
- @Retention(RetentionPolicy.RUNTIME) : 리플렉션 등을 활용할 수 있도록 런타임까지 애노테이션 정보가 남아있음

 

3) 어노테이션을 실행시킬 resolver 구현

이제 이것이 실행될 수 있도록 resolver를 구현한다

package hello.login.web.argumentresolver;

import hello.login.domain.member.Member;
import hello.login.web.SessionConst;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

@Slf4j
public class LoginMemberArgumentResolver implements
        HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        log.info("supportsParameter 실행");
        boolean hasLoginAnnotation =
                parameter.hasParameterAnnotation(Login.class);
        boolean hasMemberType =
                Member.class.isAssignableFrom(parameter.getParameterType());
        return hasLoginAnnotation && hasMemberType;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter,
                                  ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
                                  WebDataBinderFactory binderFactory) throws Exception {
        log.info("resolveArgument 실행");
        HttpServletRequest request = (HttpServletRequest)
        webRequest.getNativeRequest();
        HttpSession session = request.getSession(false);
        if (session == null) {
            return null;
        }
        return session.getAttribute(SessionConst.LOGIN_MEMBER);
    }
}

- supportsParameter() : @Login  애노테이션이 있으면서 Member 타입이면 해당 ArgumentResolver 가 사용된다
- resolveArgument() : 컨트롤러 호출 직전에 호출 되어서 필요한 파라미터 정보를 생성해준다. 여기서는 세션 에 있는 로그인 회원 정보인 member 객체를 찾아서 반환해준다. 이후 스프링MVC는 컨트롤러의 메서드를 호출 하면서 여기에서 반환된 member 객체를 파라미터에 전달해준다.

4) 스프링 컨테이너에 등록

마지막으로 생성한 LoginMemberArgumentResolver를 빈으로 등록한다.

@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
     public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
         resolvers.add(new LoginMemberArgumentResolver());
     }
}

 

 

[Exception]

REST API를 활용할때 효율적으로 에러처리 하는 방법위주

예외가 발생해 서블릿을 넘어 WAS 까지 전달되면 http status code는 500으로 처리된다.

1) 스프링부트 기본 오류 처리

스프링 부트의 기본 설정은 오류 발생시 /error를 오류 페이지로 요청하고, BasicErrorController는 이 경로를 기본으로 받는다.

이 기본경로는 .applications 파일에서 server.erorr.path 로 변경 가능하다.

 

스프링 부트는 BasicErrorController가 제공하는 기본 정보들을 활용하여 오류 API를 생성해준다. 노출되는 오류 메세지는 조절 가능하다.

server.error.include-binding-errors=always

server.error.include-exception=true

server.error.include-message=always

server.error.include-stacktrace=always

 

보안상 가릴껀 가리도록 한다!

 

2) API 예외 처리 : HandlerExceptionResolver

스프링 MVC는 컨트롤러(핸들러) 밖으로 예외가 던져진 경우 예외를 해결하고, 동작을 새로 정의할 수 있는 방법을 제공한다. 컨트롤러 밖으로 던져진 예외를 해결하고, 동작 방식을 변경하고 싶으면 HandlerExceptionResolver 를 사용하면 된다. 줄여서 ExceptionResolver 라 한다.

 

이때 ExceptionResolver로 적절한 에러처리를 하였어도 Interceptor의 postHandle()은 호출되지 않는다.

> 사용자 정의 예외 추가하기

- 나만의 Exception 종류를 추가한다.

package hello.exception.exception;

public class UserException extends RuntimeException {
    public UserException() {
        super();
    }

    public UserException(String message) {
        super(message);
    }

    public UserException(String message, Throwable cause) {
        super(message, cause);
    }

    public UserException(Throwable cause) {
        super(cause);
    }

    protected UserException(String message, Throwable cause, boolean
            enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

 

> HandlerExceptionResolver 구현하기

- 나만의 Resolver 만들고 에러종류에 따라 status code 지정하기

package hello.exception.resolver;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,HttpServletResponse response, Object handler, Exception ex) {
    try {
        if (ex instanceof UserException) {
            log.info("UserException resolver to 400");
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            if ("application/json".equals(acceptHeader)) {
                 Map<String, Object> errorResult = new HashMap<>();
                 errorResult.put("ex", ex.getClass());
                 errorResult.put("message", ex.getMessage());
                 String result = objectMapper.writeValueAsString(errorResult);
                 response.setContentType("application/json");
                 response.setCharacterEncoding("utf-8");
                 response.getWriter().write(result);
                 return new ModelAndView();
             } else {
                 //TEXT/HTML
                 return new ModelAndView("error/400");
             }
        }
    } catch (IOException e) {
        log.error("resolver ex", e);
    }
    return null;
    }
}

발생한 에러 종류가 IllegalArgumentException 라면 status code 를 400으로 처리하게 한다.

 

- HandlerExceptionResolver의 return 값

해당 메소드에서 return 값의 종류에 따라 DispatcherServlet의 동작 방식이 달라진다.

- 빈 ModelAndView :new ModelAndView() 처럼 빈 ModelAndView 를 반환하면 뷰를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴된다.

- ModelAndView 지정 : ModelAndViewView , Model 등의 정보를 지정해서 반환하면 뷰를 렌더링 한다.

- null : null 을 반환하면, 다음 ExceptionResolver 를 찾아서 실행한다. 만약 처리할 수 있는 ExceptionResolver 가 없으면 예외 처리가 안되고, 기존에 발생한 예외를 서블릿 밖으로 던진다.

 

- 구현한 Resolver 등록하기

WebMvcConfigurer를 통해 아래와 같이 등록한다.

/**
* 기본 설정을 유지하면서 추가 */
 @Override
 public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
     resolvers.add(new MyHandlerExceptionResolver());
}

 

3) API 예외 처리 - @ExceptionHandler : 클래스 처리

위에서 언급한 HandlerExceptionResolver의 리턴값은 ModelAndView 인데 REST API 프로젝트에서는 해당 반환값을 필요로 하지 않는다. 또한 특정 컨트롤러에서만 발생하는 예외를 별도로 처리하기도 어려웠는데. 이러한 단점을 보완하는 @ExceptionHandler 가 있다.

 

> 클래스 내에서 사용하는 @ExceptionHandler

 @Slf4j
 @RestController
 public class ApiExceptionV2Controller {
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     @ExceptionHandler(IllegalArgumentException.class)
     public ErrorResult illegalExHandle(IllegalArgumentException e) {
         log.error("[exceptionHandle] ex", e);
         return new ErrorResult("BAD", e.getMessage());
     }
 
 
     @ExceptionHandler
     public ResponseEntity<ErrorResult> userExHandle(UserException e) {
         log.error("[exceptionHandle] ex", e);
         ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
         return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
     }
     
    @GetMapping("/api2/members/{id}")
    public MemberDto getMember(@PathVariable("id") String id) {
        if (id.equals("user-ex")) {
            throw new UserException("사용자 오류");
        }
    }

 

- 에러 캐치

Get 메소드로 호출된 컨트롤러 안에서 UserException 발생 시  @ExceptionHandler에 의해 캐치되어 적절하게 에러처리가 이뤄진다. 해당 에러처리 Handle 에서 status code는 400 이 되고 ResponseEntity 로 return 되어 정상 리턴이 이뤄진다.

 

- 에러 처리 우선순위

@ExcpetionHandler 메소드를 여러개 생성할 수 있는데 캐치하는 에러의 상하관계에 따라 자식클래스가 우선순위를 가지며 에러처리가 이뤄진다. @ExcpeitonHandler의 매개변수로 해당 메서드를 통해 처리될 에러class를 정의할 수 있는데 이곳에 복수개의 등록이 가능하다.

 @ExceptionHandler({AException.class, BException.class})

만약 해당 부분을 생략 할 경우 메서드의 파라미터의 예외 종류가 지정되어 처리된다.

@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandle(UserException e) {

 

> @ExceptionHandler의 실행 흐름

- 컨트롤러를 호출한 결과 IllegalArgumentException 예외가 컨트롤러 밖으로 던져진다.

- 예외가 발생했으로 ExceptionResolver 가 작동한다. 가장 우선순위가 높은 ExceptionHandlerExceptionResolver 가 실행된다.

- ExceptionHandlerExceptionResolver 는 해당 컨트롤러에 IllegalArgumentException 을 처리 할 수 있는 @ExceptionHandler 가 있는지 확인한다.

- illegalExHandle() 를 실행한다. @RestController 이므로 illegalExHandle() 에도 @ResponseBody 가 적용된다. 따라서 HTTP 컨버터가 사용되고, 응답이 다음과 같은 JSON으로 반환된다.

- @ResponseStatus(HttpStatus.BAD_REQUEST) 를 지정했으므로 HTTP 상태 코드 400으로 응답한다.

 

 

4) API 예외 처리 - @ControllerAdvice : 모든 클래스

@ExceptionHandler 를 사용해서 예외를 깔끔하게 처리할 수 있게 되었지만, 정상 코드와 예외 처리 코드가 하나의
컨트롤러에 섞여 있다. @ControllerAdvice 또는 @RestControllerAdvice 를 사용하면 둘을 분리할 수 있다.

 

#1 컨트롤러에서 예외 처리 코드 삭제

package hello.exception.exhandler;

import hello.exception.exception.UserException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
public class ApiExceptionV2Controller {
    @GetMapping("/api2/members/{id}")
    public MemberDto getMember(@PathVariable("id") String id) {
        if (id.equals("ex")) {
            throw new RuntimeException("잘못된 사용자");
        }
        if (id.equals("bad")) {
            throw new IllegalArgumentException("잘못된 입력 값");
        }
        if (id.equals("user-ex")) {
            throw new UserException("사용자 오류");
        }
        return new MemberDto(id, "hello " + id);
    }

    @Data
    @AllArgsConstructor
    static class MemberDto {
        private String memberId;
        private String name;
    }
}

 

#2 ControllerAdvice 처리 코드 작성

@Slf4j
@RestControllerAdvice(basePackages = "hello.exception.api")
public class ExControllerAdvice {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(IllegalArgumentException.class)
    public ErrorResult illegalExHandler(IllegalArgumentException e) {
        log.error("[exceptionHandler] ex", e);
        return new ErrorResult("BAD", e.getMessage());
    }

    @ExceptionHandler
    public ResponseEntity<ErrorResult> userExHandler(UserException e) {
        log.error("[exceptionHandler] ex", e);
        ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
        return new ResponseEntity(errorResult, HttpStatus.BAD_REQUEST);
    }

    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler
    public ErrorResult exHandler(Exception e) {
        log.error("[exceptionHandler] ex", e);
        return new ErrorResult("EX", "내부 오류");
    }
}

RestControllerAdvice의 basePackage 파라미터를 통해 적용할 범위를 지정해 준다. 해당 범위 내에서 에러 발생시 위 파일내에서 처리 가능한 에러 종류라면 적절하게 처리되어진다.

> @ControllerAdvice

- @ControllerAdvice 는 대상으로 지정한 여러 컨트롤러에 @ExceptionHandler , @InitBinder 기능

을 부여해주는 역할을 한다.
- @ControllerAdvice 에 대상을 지정하지 않으면 모든 컨트롤러에 적용된다. (글로벌 적용)

- @RestControllerAdvice @ControllerAdvice 와 같고, @ResponseBody 가 추가되어 있다. @Controller , @RestController 의 차이와 같다.

 

 

반응형

+ Recent posts