10. Spring/BOOT

02. 스프링부트 (Spring Boot) Profile, JPA Naming, Exception 전략

THE HEYDAZE 2020. 7. 11. 02:45
OS Windows 10 Home 64bit 버전 1903 (OS 빌드 18362.836)
Frame Work Spring Boot 2.3.1.RELEASE

#1. Profile 전략
  Profile   설명
  local   로컬
  dev   개발
  test   테스트
  alpha   알파
  beta   베타
  prod   운영

 

  설정 방식   설명
  application.xml   xml 로 bean 생성, 주입
  @Configuration   java 코드로 bean 생성, 주입
  application.properties   properties 형식
  application.yml   yaml 형식

 

4개의 설정 방식 중 원하는 방식을 고르면 된다

보통 application.xml 은 Spring MVC, JSP 에서 사용하고

Spring Boot 는 @Configuration 과 properties 또는 @Configuration 과 yml 방식을 이용한다

 

Pom 이나 Gradle 에 해당 dependency 를 추가해야 사용가능합니다

(아래 코드들은 설명용으로 작성되어 서버 환경이 같지 않으면 오류를 발생할 수 있습니다)

 

 아래는 Spring Boot 2.3.1 기준 Gradle dependency 입니다

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // JPA
    implementation 'org.springframework.boot:spring-boot-starter-mustache' // Mustache
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' // Thymeleaf
    implementation 'org.springframework.boot:spring-boot-starter-security' // Spring Security
    compile 'pl.allegro.tech.boot:handlebars-spring-boot-starter:0.3.0' // Handlebars
    implementation 'org.springframework.boot:spring-boot-starter-validation' // Validation
    compile("org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.3") // Mybatis
    implementation 'org.springframework.boot:spring-boot-starter-web' // WEB MVC
    compileOnly 'org.projectlombok:lombok' // Lombock
    developmentOnly 'org.springframework.boot:spring-boot-devtools' // DevTools
    runtimeOnly 'mysql:mysql-connector-java' // Mysql Connector
    runtimeOnly 'com.h2database:h2' // H2 DB
    runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' // MariaDB Connector
    annotationProcessor 'org.projectlombok:lombok' // Lombock
    
    testImplementation('org.springframework.boot:spring-boot-starter-test') { // Spring Test Junit5
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    testImplementation 'org.springframework.security:spring-security-test' // Spring Security Test
//    providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat' // Tomcat War

    compile 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2:1.16' // log4jdbc
    
}

 

application.xml

@PropertySource 를 지원한다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
 
    <beans profile="local">
        <bean>
            <!-- something property -->
        </bean>
    </beans>
 
    <beans profile="dev">
        <bean>
            <!-- something property -->
        </bean>
    </beans>
 
    <beans profile="test">
        <bean>
            <!-- something property -->
        </bean>
    </beans>
 
    <beans profile="prod">
        <bean>
            <!-- something property -->
        </bean>
    </beans>
 
</beans>
 
cs

 

더보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
 
    <beans profile="local">
        <!-- Test annotation 컨픽 -->
        <context:annotation-config/>
 
        <bean id="transactionManager"
              class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <property name="dataSource" ref="dataSource"/>
        </bean>
 
        <tx:annotation-driven/>
 
        <!-- 아파치 DBCP 설정 -->
        <!-- 루트 컨텍스트 : 다른 모든 웹 구성 요소에 표시되는 공유 리소스를 정의합니다. -->
        <bean id="dataSource"
              class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
            <property name="driverClassName"
                      value="oracle.jdbc.OracleDriver" />               <!-- 오라클 -->
            <property name="url"
                      value="jdbc:oracle:thin:{{ 주소 }}:1521:ORCL" />  <!-- 연결주소 -->
            <property name="username" value="{{ 아이디 }}" />                  <!-- 계정명 -->
            <property name="password" value="{{ 비밀번호 }}" />                  <!-- 비밀번호 -->
        </bean>
        <!-- SqlSessionFactory 객체 주입 -->
        <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
            <property name="dataSource" ref="dataSource"/>
            <property name="configLocation" value="classpath:mybatis-config.xml"/> <!-- mybatis 설정 파일 -->
            <property name="mapperLocations">
                <list>
                    <!--
                        classpath    = main/resources
                        mappers      = 디렉토리 or 폴더 or 패키지
                        **           = mappers 폴더의 모든 하위폴더 까지 포함함
                        *Mappers.xml = 파일명이 Mappers.xml 로 끝나는 파일들
                        (boardMappers.xml / memberMappers.xml 둘 다 선택됨)
                    -->
                    <value>classpath:mappers/**/*Mapper.xml</value>
                </list>
            </property>
        </bean>
 
        <bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
            <property name="host" value="smtp.gmail.com" />
            <property name="port" value="587" /><!-- 465 or 25 --> <!-- 포트 -->
            <property name="username" value="{{ 이메일 }}" /> <!-- 아이디 -->
            <property name="password" value="{{ 비밀번호 }}" /> <!-- 비밀번호 -->
            <property name="defaultEncoding" value="utf-8" />
            <property name="javaMailProperties">
                <props>
                    <prop key="mail.transport.protocol">smtp</prop>
                    <prop key="mail.smtp.auth">true</prop>
                    <prop key="mail.smtp.starttls.enable">true</prop>
                    <prop key="mail.smtp.ssl.trust">smtp.gmail.com</prop> <!-- ★★★★★★★★★★★★★★★★★★★★ -->
                    <prop key="mail.debug">true</prop>
                </props>
            </property>
        </bean>
 
        <!-- SqlSession 객체 주입 -->
        <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate" destroy-method="clearCache">
            <constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory"/>
        </bean>
 
        <!-- 파일 업로드 객체 생성 -->
        <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
            <property name="maxUploadSize" value="10000000"/> <!-- 파일 크기 제한 10MB?   -->
        </bean>
 
        <context:component-scan base-package="com.margaret.myweb.domain"/>
        <context:component-scan base-package="com.margaret.myweb.persistence"/>
        <context:component-scan base-package="com.margaret.myweb.service"/>
        <context:component-scan base-package="com.margaret.myweb.criteria"/>
    </beans>
 
    <beans profile="dev">
        <bean>
            <!-- something property -->
        </bean>
    </beans>
 
    <beans profile="test">
        <bean>
            <!-- something property -->
        </bean>
    </beans>
 
    <beans profile="prod">
        <bean>
            <!-- something property -->
        </bean>
    </beans>
 
</beans>
 
cs
 

 

@Configuration

[출처] https://m.blog.naver.com/scw0531/221067648218

 

application.properties

[출처] 구글

 

 

application.yml

@PropertySource 를 지원하지 않는다

 

[ 선택 1 ] - 1개의 yml에 여러 profile 작성법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# 공통
spring:
  profiles:
    active: test
 
  datasource:
    # 히카리 커넥션 풀 (https://effectivesquid.tistory.com/entry/HikariCP-%EC%84%B8%ED%8C%85%EC%8B%9C-%EC%98%B5%EC%85%98-%EC%84%A4%EB%AA%85)
    hikari:
      maximum-pool-size: 10 # pool에 유지시킬 수 있는 최대 커넥션 수. pool의 커넥션 수가 옵션 값에 도달하게 되면 idle인 상태는 존재하지 않음.(default: 10)
      data-source-properties: #
        cachePrepStmts: true # 캐시가 기본적으로 비활성화되어있는 경우 위의 매개 변수 중 어느 것도 효과가 없습니다. 이 매개 변수를 true로 설정해야합니다.
        prepStmtCacheSize: 250 # MySQL 드라이버가 연결 당 캐시 할 준비된 문 수를 설정합니다. 기본값은 보수적 25입니다. 250-500 사이로 설정하는 것이 좋습니다.
        prepStmtCacheSqlLimit: 2048 # 드라이버가 캐시 할 준비된 SQL 문의 최대 길이입니다. MySQL 기본값은 256입니다. 경험상 특히 Hibernate와 같은 ORM 프레임 워크에서이 기본값은 생성 된 문 길이의 임계 값보다 훨씬 낮습니다. 권장 설정은 2048입니다.
        useServerPrepStmts: true # 최신 버전의 MySQL은 서버 측 준비된 명령문을 지원하므로 상당한 성능 향상을 제공 할 수 있습니다. 이 속성을 true로 설정합니다.
    #    data: classpath:data.sql # 기본 값
    #    data: classpath:*.sql # resources 폴더에서 .sql 로 끝나는 모든 파일 (하위폴더는 예외)
 
  # JSP 설정
  mvc:
    view:
      prefix: /WEB-INF/views
      suffix: .jsp
 
  # Thymeleaf 설정
  thymeleaf:
    view-names: thymeleaf/*
    prefix: classpath:/templates/
    suffix: .html
    cache: false # 파일 수정시 반영하려면 재시작 해줘야 한다. 브라우저 새로고침시 수정사항 반영을 하려면 true 설정
    check-template-location: true # 생략가능
 
  # Mustache 설정
  mustache:
    view-names: mustache/*
    prefix: classpath:/templates/
    suffix: .mustache
    cache: false
    check-template-location: true
 
  # Handlebars 설정
  handlebars:
    view-names: handlebars/*
    prefix: classpath:/templates/
    suffix: .hbs
    cache: false
    check-template-location: true
 
  # mybatis 설정
  mybatis:
    type-aliases-package: com.example.dom.*.vo # VO 클래스들이 @Alias("") 로 작성 된 경우 탐색할 패키지 지정
    mapper-locations: mybatis/**/*.xml # 쿼리가 작성된 mapper.xml 검색 대상 - ** 은 모든 하위 폴더에서 찾는다, *.xml 은 .xml 로 끝나는 파일들
 
  # JSP 파일 서버 재시작 없이 바로 적용하기
  devtools:
    livereload:
      enabled: true
 
  #log4jdbc 설정
  log4jdbc:
    spylogdelegator:
      name: net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
    dump:
      sql:
        maxlinelength: 0
 
 
---
 
# 로컬
spring:
  profiles: local # profile 명 설정 - gradle build 또는 profiles.include 또는 @Profile 을 이용할 때 사용
 
  # h2 데이터베이스 설정
  h2:
    console:
      enabled: true
      path: /h2-console
 
  # DataSource 설정
  datasource:
    driver-class-name: org.h2.Driver # DB Driver (생략가능)
    url: jdbc:h2:mem:default # DB URL (메모리 또는 DB 서버)
 
 
  # JPA 설정
  jpa:
    show-sql: true # JPA 쿼리문 출력
    hibernate:
      ddl-auto: create # 서버 재시작 시 DB 테이블 삭제 후 생성 (default) - 데이터 유지 X
      naming:
        physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy # default
    properties:
      hibernate:
        format_sql: true # 쿼리문 출력 시 sql 개행처리 여부 (true 쿼리문 보기 쉬워진다)
    open-in-view: false # 기본값이 true 이기 때문에 LazyInitializationException 처리를 위해 false (https://kingbbode.tistory.com/27)
 
 
# Slf4j 설정
logging:
  level:
    root: info # 기본 로그는 debug 이상부터 찍힌다
    org:
      hibernate:
        type:
          descriptor:
            sql: trace # 하이버네이트 sql 은 trace 이상부터 찍힌다
 
server:
  port: 8080 # 서버 포트 설정 http://localhost:8080/
  error:
    path: /error # 서버 에러 발생 시 @GetMapping 경로
 
---
 
# 개발
spring:
  profiles: dev
 
  # h2 데이터베이스 설정
  h2:
    console:
      enabled: true
      path: /h2-console
 
  # DataSource 설정
  datasource:
    driver-class-name: org.h2.Driver # DB Driver (생략가능)
    url: jdbc:h2:./data/data # DB URL (ContextRoot/data/data.sql ? 에 저장)
 
 
  # JPA 설정
  jpa:
    show-sql: true # JPA 쿼리문 출력
    hibernate:
      ddl-auto: create # 서버 재시작 시 DB 테이블 삭제 후 생성 (default) - 데이터 유지 X
      naming:
        physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy # default
    properties:
      hibernate:
        format_sql: true # 쿼리문 출력 시 sql 개행처리 여부 (true 쿼리문 보기 쉬워진다)
    open-in-view: false # 기본값이 true 이기 때문에 LazyInitializationException 처리를 위해 false (https://kingbbode.tistory.com/27)
 
# Slf4j 설정
logging:
  level:
    root: info # 기본 로그는 debug 이상부터 찍힌다
    org:
      hibernate:
        type:
          descriptor:
            sql: trace # 하이버네이트 sql 은 trace 이상부터 찍힌다
 
server:
  port: 8081
  error:
    path: /error # 서버 에러 발생 시 @GetMapping 경로
 
---
 
# 테스트
spring:
  profiles: test
 
  # DataSource 설정
  datasource:
    driver-class-name: org.mariadb.jdbc.Driver # DB Driver (생략가능)
    url: jdbc:mariadb://localhost:3306/test?characterEncoding=UTF-8&serverTimezone=UTC&autoReconnect=true&useSSL=false
#    url: jdbc:log4jdbc:mariadb://localhost:3306/test?characterEncoding=UTF-8&serverTimezone=UTC&autoReconnect=true&useSSL=false
    # DB URL (해당 db url 에 접속) - log4jdbc 로 됐기 때문에 TEST 코드에서만 작동함 (WEB 단에서 요청 시 DB 접속이 불가능)
    username: root
    password: root
 
 
  # JPA 설정
  jpa:
    show-sql: true # JPA 쿼리문 출력
    hibernate:
      ddl-auto: update # SessionFactory가 올라갈 때 Object를 검사하여 테이블을 alter 시킨다 - 데이터 유지 O
      naming:
        physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy # default
    properties:
      hibernate:
        format_sql: true # 쿼리문 출력 시 sql 개행처리 여부 (true 쿼리문 보기 쉬워진다)
    open-in-view: false # 기본값이 true 이기 때문에 LazyInitializationException 처리를 위해 false (https://kingbbode.tistory.com/27)
 
# Slf4j 설정
logging:
  level:
    root: error # 기본 로그는 debug 이상부터 찍힌다
    org:
      hibernate:
        type:
          descriptor:
            sql: trace # 하이버네이트 sql 은 trace 이상부터 찍힌다
 
server:
  port: 8082
  error:
    path: /error # 서버 에러 발생 시 @GetMapping 경로
 
---
 
# 운영
spring:
  profiles: prod
 
  # DataSource 설정
  datasource:
    driver-class-name: org.mariadb.jdbc.Driver # DB Driver (생략가능)
    url: jdbc:mariadb://localhost:3306/test?characterEncoding=UTF-8&serverTimezone=UTC&autoReconnect=true&useSSL=false # DB URL (해당 db url 에 test 스키마에 접속)
    username: root # DB 접속 아이디
    password: root # DB 접속 패스워드
 
  # JPA 설정
  jpa:
    show-sql: true # JPA 쿼리문 출력
    hibernate:
      ddl-auto: update # SessionFactory가 올라갈 때 Object를 검사하여 테이블을 alter 시킨다 - 데이터 유지 O
      naming:
        physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy # default
    properties:
      hibernate:
        format_sql: true # 쿼리문 출력 시 sql 개행처리 여부 (true 쿼리문 보기 쉬워진다)
    open-in-view: false # 기본값이 true 이기 때문에 LazyInitializationException 처리를 위해 false (https://kingbbode.tistory.com/27)
 
# Slf4j 설정
logging:
  level:
    root: info # 기본 로그는 debug 이상부터 찍힌다
 
server:
  port: 8083
  error:
    path: /error # 서버 에러 발생 시 @GetMapping 경로
 
---
 
cs

 

[ 선택 2 ] - profile 별로 yml 나누는 방법

- application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# 공통
spring:
  profiles: core
 
  datasource:
    # 히카리 커넥션 풀 (https://effectivesquid.tistory.com/entry/HikariCP-%EC%84%B8%ED%8C%85%EC%8B%9C-%EC%98%B5%EC%85%98-%EC%84%A4%EB%AA%85)
    hikari:
      maximum-pool-size: 10 # pool에 유지시킬 수 있는 최대 커넥션 수. pool의 커넥션 수가 옵션 값에 도달하게 되면 idle인 상태는 존재하지 않음.(default: 10)
      data-source-properties: #
        cachePrepStmts: true # 캐시가 기본적으로 비활성화되어있는 경우 위의 매개 변수 중 어느 것도 효과가 없습니다. 이 매개 변수를 true로 설정해야합니다.
        prepStmtCacheSize: 250 # MySQL 드라이버가 연결 당 캐시 할 준비된 문 수를 설정합니다. 기본값은 보수적 25입니다. 250-500 사이로 설정하는 것이 좋습니다.
        prepStmtCacheSqlLimit: 2048 # 드라이버가 캐시 할 준비된 SQL 문의 최대 길이입니다. MySQL 기본값은 256입니다. 경험상 특히 Hibernate와 같은 ORM 프레임 워크에서이 기본값은 생성 된 문 길이의 임계 값보다 훨씬 낮습니다. 권장 설정은 2048입니다.
        useServerPrepStmts: true # 최신 버전의 MySQL은 서버 측 준비된 명령문을 지원하므로 상당한 성능 향상을 제공 할 수 있습니다. 이 속성을 true로 설정합니다.
    #    data: classpath:data.sql # 기본 값
    #    data: classpath:*.sql # resources 폴더에서 .sql 로 끝나는 모든 파일 (하위폴더는 예외)
 
  # JSP 설정
  mvc:
    view:
      prefix: /WEB-INF/views
      suffix: .jsp
 
  # Thymeleaf 설정
  thymeleaf:
    view-names: thymeleaf/*
    prefix: classpath:/templates/
    suffix: .html
    cache: false # 파일 수정시 반영하려면 재시작 해줘야 한다. 브라우저 새로고침시 수정사항 반영을 하려면 true 설정
    check-template-location: true # 생략가능
 
  # Mustache 설정
  mustache:
    view-names: mustache/*
    prefix: classpath:/templates/
    suffix: .mustache
    cache: false
    check-template-location: true
 
  # Handlebars 설정
  handlebars:
    view-names: handlebars/*
    prefix: classpath:/templates/
    suffix: .hbs
    cache: false
    check-template-location: true
 
  # mybatis 설정
  mybatis:
    type-aliases-package: com.example.dom.*.vo # VO 클래스들이 @Alias("") 로 작성 된 경우 탐색할 패키지 지정
    mapper-locations: mybatis/**/*.xml # 쿼리가 작성된 mapper.xml 검색 대상 - ** 은 모든 하위 폴더에서 찾는다, *.xml 은 .xml 로 끝나는 파일들
 
  # JSP 파일 서버 재시작 없이 바로 적용하기
  devtools:
    livereload:
      enabled: true
 
  #log4jdbc 설정
  log4jdbc:
    spylogdelegator:
      name: net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
    dump:
      sql:
        maxlinelength: 0
cs

 

- application-local.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 로컬
profile: local # 사용자 환경변수
 
spring:
  profiles:
    include: core
 
  # h2 데이터베이스 설정
  h2:
    console:
      enabled: true
      path: /h2-console
 
  # DataSource 설정
  datasource:
    driver-class-name: org.h2.Driver # DB Driver (생략가능)
    url: jdbc:h2:mem:default # DB URL (메모리 또는 DB 서버)
 
 
  # JPA 설정
  jpa:
    show-sql: true # JPA 쿼리문 출력
    hibernate:
      ddl-auto: create # 서버 재시작 시 DB 테이블 삭제 후 생성 (default) - 데이터 유지 X
      naming:
        physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy # default
    properties:
      hibernate:
        format_sql: true # 쿼리문 출력 시 sql 개행처리 여부 (true 쿼리문 보기 쉬워진다)
    open-in-view: false # 기본값이 true 이기 때문에 LazyInitializationException 처리를 위해 false (https://kingbbode.tistory.com/27)
 
 
# Slf4j 설정
logging:
  level:
    root: info # 기본 로그는 debug 이상부터 찍힌다
    org:
      hibernate:
        type:
          descriptor:
            sql: trace # 하이버네이트 sql 은 trace 이상부터 찍힌다
 
server:
  port: 8080 # 서버 포트 설정 http://localhost:8080/
  error:
    path: /error # 서버 에러 발생 시 @GetMapping 경로
 
cs

 

- application-dev.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 개발
profile: dev # 사용자 환경변수
 
spring:
  profiles:
    include: core
 
  # h2 데이터베이스 설정
  h2:
    console:
      enabled: true
      path: /h2-console
 
  # DataSource 설정
  datasource:
    driver-class-name: org.h2.Driver # DB Driver (생략가능)
    url: jdbc:h2:./data/data # DB URL (ContextRoot/data/data.sql ? 에 저장)
 
 
  # JPA 설정
  jpa:
    show-sql: true # JPA 쿼리문 출력
    hibernate:
      ddl-auto: create # 서버 재시작 시 DB 테이블 삭제 후 생성 (default) - 데이터 유지 X
      naming:
        physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy # default
    properties:
      hibernate:
        format_sql: true # 쿼리문 출력 시 sql 개행처리 여부 (true 쿼리문 보기 쉬워진다)
    open-in-view: false # 기본값이 true 이기 때문에 LazyInitializationException 처리를 위해 false (https://kingbbode.tistory.com/27)
 
# Slf4j 설정
logging:
  level:
    root: info # 기본 로그는 debug 이상부터 찍힌다
    org:
      hibernate:
        type:
          descriptor:
            sql: trace # 하이버네이트 sql 은 trace 이상부터 찍힌다
 
server:
  port: 8081
  error:
    path: /error # 서버 에러 발생 시 @GetMapping 경로
cs

 

- application-test.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 테스트
profile: test # 사용자 환경변수
 
spring:
  profiles:
    include: core
 
  # DataSource 설정
  datasource:
    driver-class-name: org.mariadb.jdbc.Driver # DB Driver (생략가능)
    url: jdbc:mariadb://localhost:3306/test?characterEncoding=UTF-8&serverTimezone=UTC&autoReconnect=true&useSSL=false
    #    url: jdbc:log4jdbc:mariadb://localhost:3306/test?characterEncoding=UTF-8&serverTimezone=UTC&autoReconnect=true&useSSL=false
    # DB URL (해당 db url 에 접속) - log4jdbc 로 됐기 때문에 TEST 코드에서만 작동함 (WEB 단에서 요청 시 DB 접속이 불가능)
    username: root
    password: root
 
 
  # JPA 설정
  jpa:
    show-sql: true # JPA 쿼리문 출력
    hibernate:
      ddl-auto: update # SessionFactory가 올라갈 때 Object를 검사하여 테이블을 alter 시킨다 - 데이터 유지 O
      naming:
        physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy # default
    properties:
      hibernate:
        format_sql: true # 쿼리문 출력 시 sql 개행처리 여부 (true 쿼리문 보기 쉬워진다)
    open-in-view: false # 기본값이 true 이기 때문에 LazyInitializationException 처리를 위해 false (https://kingbbode.tistory.com/27)
 
# Slf4j 설정
logging:
  level:
    root: error # 기본 로그는 debug 이상부터 찍힌다
    org:
      hibernate:
        type:
          descriptor:
            sql: trace # 하이버네이트 sql 은 trace 이상부터 찍힌다
 
server:
  port: 8082
  error:
    path: /error # 서버 에러 발생 시 @GetMapping 경로
 
cs

 

- application-prod.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 운영
profile: prod # 사용자 환경변수
 
spring:
  profiles:
    include: core
 
  # DataSource 설정
  datasource:
    driver-class-name: org.mariadb.jdbc.Driver # DB Driver (생략가능)
    url: jdbc:mariadb://localhost:3306/test?characterEncoding=UTF-8&serverTimezone=UTC&autoReconnect=true&useSSL=false # DB URL (해당 db url 에 test 스키마에 접속)
    username: root # DB 접속 아이디
    password: root # DB 접속 패스워드
 
  # JPA 설정
  jpa:
    show-sql: true # JPA 쿼리문 출력
    hibernate:
      ddl-auto: update # SessionFactory가 올라갈 때 Object를 검사하여 테이블을 alter 시킨다 - 데이터 유지 O
      naming:
        physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy # default
    properties:
      hibernate:
        format_sql: true # 쿼리문 출력 시 sql 개행처리 여부 (true 쿼리문 보기 쉬워진다)
    open-in-view: false # 기본값이 true 이기 때문에 LazyInitializationException 처리를 위해 false (https://kingbbode.tistory.com/27)
 
# Slf4j 설정
logging:
  level:
    root: info # 기본 로그는 debug 이상부터 찍힌다
 
server:
  port: 8083
  error:
    path: /error # 서버 에러 발생 시 @GetMapping 경로
 
cs

 

[환경변수 출력하기]

선택 2 의 경우

[ 선택 2 ] 방법으로 작성한 경우 profile 을 위 그림 처럼 지정해주고 실행해야 적용이 된다 

[ 선택 1 ] 에서는 각 각에 profile 명을 준 후 '# 공통' 에서 profile active: local 로 주었기 때문에 시작 시 local 로 설정된다

[ 선택 2 ] 는 application.yml 에만 profile 명이 있고, 나머지는 -local, -dev, -test, -prod 은 profile 명이 없는 대신

application.yml 의 profile 명 core 를 include (포함) 하는 방식으로 한다

 

[참고 1]

application.yml 이 @Configuration 보다 먼저 지정된다.

때문에 application.yml 과 @Configuration 이 동일한 대상에 대해 설정 할 경우

@Configuration 으로만 설정이 된다.

 

[참고 2]

application.properties 에서는 지원이 되나 application.yml 에서는 지원이 안되는 것도 있습니다

( 예를들어 handlebars 설정? properties 에서는 handlebars 자동완성이 있는데, yml 은 자동완성이 없어서 ... )

 

[참고 3]

application.properties 와 application.yml 은 외부에서 설정을 할 수 있다.

예를들어 Gradle 에서 빌드 할 때에도 사용이 가능하며, .jar 로 빌드 시에도 설정이 가능하다

[출처] https://www.latera.kr/reference/java/2019-09-29-spring-boot-config-externalize/

 

[Spring Boot] 외부에서 설정 주입하기 - Increment

4.2. 외부에서 설정 주입하기 Spring boot 애플리케이션의 설정 값을 외부에서 전달하는 방법을 다룹니다. 기준 버전은 2.2x입니다. Spring Boot는 동일한 애플리케이션 코드를 다른 환경에서 작업할 수

www.latera.kr

 


[ Hikari Connection Pool ]

 

HikariCP 세팅시 옵션 설명

HikariCP 옵션 jdbcUrl, username, password는 너무 기본적인 내용이라 생략하겠습니다. HikariCP설정의 시간 단위는 ms입니다. autoCommit: auto-commit설정 (default: true) connectionTimeout: pool에서 커넥..

effectivesquid.tistory.com

 

[ ddl-auto ]

  방식   설명
  create    SessionFactory 가 올라갈 때 테이블을 지우고 새로 만듬 - 데이터 삭제
  create-drop   SessionFactory 가 올라 갈 때 테이블을 생성, SessionFactory 가 내려가면 테이블 삭제 -  데이터 삭제
  update   SessionFactory가 올라갈 때 Object를 검사하여 테이블을 alter 시킨다. - 데이터는 유지
  validate   update처럼 Object를 검사하지만, 스키마는 아무것도 건드리지 않고,
  Object와 스키마의 정보가 다르다면 에러를 발생시킨다. - 데이터 유지

 

[open-in-view 참고 바람]

 

 

Spring - Open Session In View

Spring에서 ORM을 사용하여 개발을 하며, Transaction 을 이해할 때 쯔음 닥쳐온 혼란이 있습니다. 지인에게 자신있게 Transaction 을 설명해주기 위해 Spring Boot로 빠르게 어플리케이션을 올렸고 @GetMapping(

kingbbode.tistory.com

 

[log back]

 

 

[Spring Boot] 07. Logback 구성하기

지금까지 많지도 않지만 적지도않은 단계를 진행하였습니다. 이제는 Server가 동작하는데 필요한 log가 남겨지도록 구성을 해보겠습니다. 현재는 project내 아무런 log가 남지 않습니다. 비정상적으�

ayoteralab.tistory.com

 

 

(Spring Boot)Logging과 Profile 전략

서론spring-boot에서는 Logback을 확장해서 편리한 logging 설정을 할 수 있다. application.yml의 설정을 읽어 오거나 특정 profile의 동작을 수행할 수 있다. 이 글에서는 Spring Boot에서 어떻게 logging 설정을 ��

supawer0728.github.io

 

[기타 참고한 사이트]

 

 

#2. JPA Naming 전략
  Naming   설명
  SpringPhysicalNamingStrategy [기본값]   camel case를 underscore 형태로 변경 
  PhysicalNamingStrategyStandardImpl   변수 이름을 그대로 사용

 

SpringPhysicalNamingStrategy

이 방식은 memberId 인 경우 DB 컬럼명이 memberId 가 아닌 member_id 로 된다

 

PhysicalNamingStrategyStandardImpl

이 방식은 memberId 인 경우 DB 컬럼명이 memberId 로 된다

 

[자세한 내용은 아래 참고]

 

Springboot jpa & Hibernate Naming Strategy(네이밍 전략)

Springboot jpa & Hibernate Naming Strategy 전략 어느날 부터인가 컬럼명이 카멜케이스로 나오고 있다. 왜이러지? Springboot 특정 버전 이상부터 프로퍼티 설정이 변경되었다. 프로젝트에 대소문자 구별이 ��

mycup.tistory.com

 

#3. Exception 전략

참고한 블로그

 

Spring Guide - Exception 전략 - Yun Blog | 기술 블로그

Spring Guide - Exception 전략 - Yun Blog | 기술 블로그

cheese10yun.github.io

 

[exception 관리하지 않은 경우]

이미지 클릭

위 GIF 를 보다시피 여러 요청에 관해 응답은 제 각각으로 응답한다

이럴 경우 클라이언트(웹, 모바일)에서 에러 로직을 공통으로 처리하는 데 있어 어려움이 있다

특히나, 비지니스 로직의 exception 들은 상황에 맞게 응답을 처리해주어야 한다.

 

기본적으로 exception 이 발생하면 서버에러(500) 을 응답한다.

비지니스 로직 (중복된 이메일, 비밀번호 불일치, 존재하지 않는 데이터) exception 또한 서버에러(500) 으로 처리하는데,

비밀번호 일치와 같은 exception 같은 문제는 잘못된 요청(400) 을 응답하는 게 적절하다

 

 

#1. GlobalExceptionHandler

exception 을 메소드 단위 별로 처리하는 방법도 있지만, 그러면 오류를 관리하기 힘들기 때문에

GlobalExceptionHanlder 라는 클래스를 만들어 모든 exception 들을 관리하고 처리하는 것이 좋다

[코드]

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /** Exception */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleException(Exception e) {
        log.error("handleException", e);
        return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
    }

}

일단 exception 을 GlobalExceptionHanlder 클래스에서 관리하게 되었다.

log.error 를 이용하여 로그를 찍는다.

"handleException" 은 이 exception 은 @ExceptionHandler 로 handle 링 된 메소드임을 알기 위해 써준 것이다.

@ContollerAdvice 가 모든 exception 처리를 GlobalExceptionHanlder 로 하게끔 하였다.

속성값으로 패키지별로 설정할 수 도 있다.

@ExceptionHandler 로 원하는 exception 클래스를 처리할 수 있도록 하였다.

이제 exception 발생 시 에는 오류 메시지와 잘못된 요청(400) 을 응답하게 된다.

 

[MethodArgumentNotValidException 요청 실패 결과]

위와 같이 설정 후 잘못된 요청을 해보았더니 오류 메시지가 원치않게 많이 나왔다.

exception 에 대해서 원하는 부분만을 따로 처리해주지 않았기 때문에 불필요한 정보까지 전부 나온 것이다.

 

클라이언트에서 처리할 수 있도록 필요한 정보만을 응답해줄 수 있는 클래스를 만들어야한다.

 

 

#2. ErrorResponse

[ErrorResponse 코드]

@Getter
@NoArgsConstructor
public class ErrorResponse {

    // 응답 메시지
    private String message;
    
    // 응답 상태
    private int status;
    
    // 응답 코드
    private String code;
}

 

모든 exception 에서 필요한 정보만을 ErrorResponse 클래스를 통해 생성 해 클라이언트에 응답할 것 이다.

응답 메시지는 클라이언트(사용자)에게 알려줄 오류 메시지이고,

응답 상태는 400 잘못된 요청인지 403 권한이 없는지 500 서버오류 인지 알려 주는 것이다.

응답 코드는 나중에 어떤 exception 인지 찾기 위한 해당 서버에 대한 애플리케이션 오류 코드 이다.

 

[GlobalExceptionHandler 코드]

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /** Exception */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(Exception e) {
        log.error("handleException", e);
        final ErrorResponse response = new ErrorResponse("서버 에러", 500, "C001");
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

}

 

[MethodArgumentNotValidException 요청 실패 결과]

이번에는 아까와 달리 알기 쉽게 응답되었다.

여기서 부족한 점은 응답 메시지 내용이 어떤 오류의 내용인지 알기 어렵다

 

자세한 응답을 보기 위해, 특정 exception 에 대해 @ExceptionHandler 로 handling 할 것 이다.

아이디 중복에 대한 exception 을 만들고 handler 메소드를 생성해본다.

 

[MemberIdDuplicateException 코드]

public class MemberIdDuplicateException extends RuntimeException {

    public MemberIdDuplicateException(String memberId) {
        super(memberId + " 는 중복된 아이디 입니다");
    }
}

 

[GlobalExceptionHandler 코드]

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(MemberIdDuplicateException.class)
    public ResponseEntity<ErrorResponse> handleMemberIdDuplicateException(MemberIdDuplicateException e) {
        log.error("handleMemberIdDuplicateException", e);
        final ErrorResponse response = new ErrorResponse(e.getMessage(), 400, "C002");
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

    /** Exception */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(Exception e) {
        log.error("handleException", e);
        final ErrorResponse response = new ErrorResponse("서버 에러", 500, "C001");
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

}

 

[MemberIdDuplicateException 요청 실패 결과]

MemberIdDuplicateException 을 handling 함으로써 자세한 정보를 알 수 있게 되었다.

그런데 여기서도 부족한 점을 발견하게 된다.

2개의 메소드를 작성해보니 ErrorResponse 에 대해 많은 의존성을 갖게되었다.

MemberIdDuplicateException 는 BusinessException 라고 볼 수 있는 데,

이러한 BusinessException 는 애플리케이션이 커질 수록 증가하게 되는 데, 결국 

GlobalExceptionHandlerMemberIdDuplicateException 과 같은 핸들링을 여러개 추가해야 하고,

ErrorResponse의 매개변수 인 status 와 code 또한 계속 적어주어야 한다.

 

이런점을 보완하기 위해 message, status, code 매개변수에 대한 책임들을 exception 에게 주도록 해야 한다.

이를 위해 ErrorCode Enum 클래스, InvalidValueException 클래스, EntityNotFoundException 클래스,

BusinessException 클래스를 만들 것이다.

 

비지니스 상에서 일어날 수 있는 오류들(BusinessException)은 크게 2가지로 나뉜다.

첫 번째는 잘못된 요청(InvalidValueException) 과 두 번째는 없는 데이터에 대한 요청(EntityNotFoundException) 이다

MemberIdDuplicateException 는 존재하지만 비지니스적인 이유로 동일한 id로는 가입할 수 없도록 막은 것이기 때문에

잘못된 요청 (InvalidValueException)에 포함된다. (상속)

 

예시 - 커스텀 Exception 구조 (BusinessException)

[출처] https://cheese10yun.github.io/spring-guide-exception/

 

[MemberIdDuplicateException 코드]

public class MemberIdDuplicateException extends InvalidValueException {

    public MemberIdDuplicateException(String memberId) {
        super(memberId + " 는 중복된 아이디 입니다");
    }
}

 

[InvalidValueException 코드]

public class InvalidValueException extends BusinessException {

    public InvalidValueException(String value) {
        super(value, 400, "C002");
    }

}

 

[BusinessException 코드]

public class BusinessException extends RuntimeException {

    private int status;
    private String code;

    public BusinessException(String message, int status, String code) {
        super(message);
        this.status = status;
        this.code = code;
    }

    // @Override public String getMessage() {...}

    public int getStatus() {
        return status;
    }

    public String getCode() {
        return code;
    }
}

 

[GlobalExceptionHandler 코드]

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
        log.error("handleBusinessException", e);
        final ErrorResponse response = new ErrorResponse(e.getMessage(), e.getStatus(), e.getCode());
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

    /** Exception */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(Exception e) {
        log.error("handleException", e);
        final ErrorResponse response = new ErrorResponse("서버 에러", 500, "C001");
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

}

일단 비지니스 상의 exception 들이 늘어날 때 마다 GlobalExceptionHandler 까지 작성할 필요가 사라졌다.

또한 message, status, code 에 대한 책임이 해당 exception 에게 갔다. 그래서 GlobalExceptionHandler 에서

그 해당 exception 에 대해서 어떤 message 를? 어떤 status 를? 어떤 code 를? 생각 할 필요가 사라졌다

이것은 SOLID 원칙을 준수하는 것이다

 

이제 ErrorCode Enum 클래스 활용하여 반복되는 message, status, code 를 대체할 것이다.

 

[ErrorCode 코드]

@Getter
public enum ErrorCode {
    INVALID_INPUT_VALUE(400, "C002", "잘못된 요청"),
    INTERNAL_SERVER_ERROR(400, "C001", "서버 오류")
    ;

    private String message;
    private int status;
    private String code;

    ErrorCode(int status,  String code, String message) {
        this.status = status;
        this.code = code;
        this.message = message;
    }
}

 

[InvalidValueException 코드]

public class InvalidValueException extends BusinessException {

    public InvalidValueException(String value) {
        super(value, ErrorCode.INVALID_INPUT_VALUE);
    }

}

status 와 code 를 ErrorCode.INVALID_INPUT_VALUE 로 대체 하였다

 

[BusinessException 코드]

public class BusinessException extends RuntimeException {

    private ErrorCode errorCode;

    public BusinessException(String message, ErrorCode errorCode) {
        super(message);
        this.errorCode = errorCode;
    }

    public ErrorCode getErrorCode() {
        return errorCode;
    }
}

status 와 code 에 대한 생성자는 지우고

message 와 ErrorCode 를 받는 생성자를 새로 작성한다.

멤버변수 status 와 code 는 지우고 private ErrorCode errorCode 로 대체한다.

 

[GlobalExceptionHandler 코드]

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
        log.error("handleBusinessException", e);
        final ErrorResponse response = ErrorResponse.of(e.getErrorCode());
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

    /** Exception */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(Exception e) {
        log.error("handleException", e);
        final ErrorResponse response = ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR);
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

}

이제 ErrorResponse 를 생성할 때 message, status, code 를 쓸 필요 없이,

모두 포함된 ErrorCode 를 매개변수로 주면 된다.

 

static 메소드를 이용해 생성자를 생성한 이유는 아래 사이트 참고바람

 

 

객체 생성 정적 팩토리 메서드를 쓰는게 왜 유리한가 (Effective java 3th - Item1)

객체 생성 정적 팩토리 메서드를 쓰는게 왜 유리한가 (Effective java 3th - Item1)

blog.javarouka.me

 

[ErrorResponse 코드]

@Getter
@NoArgsConstructor
public class ErrorResponse {

    private String message;
    private int status;
    private String code;

    private ErrorResponse(final ErrorCode code) {
        this.message = code.getMessage();
        this.status = code.getStatus();
        this.code = code.getCode();
    }

    public static ErrorResponse of(final ErrorCode code) {
        return new ErrorResponse(code);
    }
}

message, status, code 매개변수로 생성되는 생성자를 지우고

ErrorCode 매개변수로 생성되는 생성자를 작성한다.

 

[MemberIdDuplicateException 요청 실패 결과]

 

이제 추가적으로 아래의 기본적인 exception 들을 handling 할 것이다. 

  클래스   상태   원인
  MethodArgumentNotValidException   400   @Valid 오류
  BindException   400   @ModelAttribute 오류
  MethodArgumentTypeMismatchException   400   @RequestParam enum 오류
  HttpRequestMethodNotSupportedException   405   지원하지 않는 HTTP method 호출 오류
  AccessDeniedException   403   Authentication 객체가 필요한 권한을 보유하지 않은 경우
  BusinessException   400   비지니스 오류 (사용자 커스텀 Exception)
  Exception   500   그 외 오류들

 

[상태 코드 참고]

 

01. http Status

OS Windows 10 Home 64bit 버전 1903 (OS 빌드 18362.836) 자주 사용되는 상태코드는 ★ 표시 정보 100 Continue 이 임시적인 응답은 지금까지의 상태가 괜찮으며 클라이언트가 계속해서  요청을 하거나 이미 요

theheydaze.tistory.com

 


MethodArgumentNotValidException

이 Exception 은 유효성 검사를 할 때 발생하는 exception 이다

 

Controller

 

Member

유효성 검사에 실패하면 MethodArgumentNotValidException 을 발생한다

유효성검사를 하려면 spring-boot-starter-validation 의존성 또는 validation-api 의존성을 추가해야한다.

 

MethodArgumentNotValidException 는 BusinessException 과 달리 ErrorCode 클래스를 멤버변수로 갖고 있지않아

ErrorResponse 의 생성자로 생성할 수 없다. 때문에 ErrorResponse 에 새로운 생성자를 만들어준다.

 

[GlobalExceptionHandler 코드]

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /** @Valid */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error("handleMethodArgumentNotValidException", e);
        final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE,e.getMessage());
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
        log.error("handleBusinessException", e);
        final ErrorResponse response = ErrorResponse.of(e.getErrorCode());
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

    /** Exception */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(Exception e) {
        log.error("handleException", e);
        final ErrorResponse response = ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR);
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

}

status 와 code 는 ErrorCode.INVALID_INPUT_VALUE 로 대체하였고, message 는 e.getMessage 로 하였다.

MethodArgumentNotValidException 가 ErrorCode 를 갖고있지않고, 어떤 오류내용을 갖고 있는 지 모르기 때문에

2개의 매개변수를 갖는 ErrorResponse 생성자를 작성한 것이다.

 

[ErrorResponse 코드]

@Getter
@NoArgsConstructor
public class ErrorResponse {

    private String message;
    private int status;
    private String code;

    private ErrorResponse(final ErrorCode code) {
        this.message = code.getMessage();
        this.status = code.getStatus();
        this.code = code.getCode();
    }

    public ErrorResponse(ErrorCode code, String message) {
        this.message = message;
        this.status = code.getStatus();
        this.code = code.getCode();
    }

    public static ErrorResponse of(final ErrorCode code) {
        return new ErrorResponse(code);
    }

    public static ErrorResponse of(ErrorCode errorCode, String message) {
        return new ErrorResponse(errorCode, message);
    }
}

 

 

[MethodArgumentNotValidException 요청 실패 결과]

 

위 처럼  e.getMessage() 만 넘긴다면 불필요한 내용도 응답이 된다.

여기서 필요한 정보만 가져오기 위해 MethodArgumentNotValidExceptionBindingResult.getFieldErrors() 를

사용할 것이다. 이 메소드를 사용하면 List<FieldError> 를 리턴해준다

FieldError 에는 message, field, code, objectName 등 자세한 내용을 알 수 있다.

FieldError 의 get 메소드

 

[GlobalExceptionHandler 코드]

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /** @Valid */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error("handleMethodArgumentNotValidException", e);
        final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE,e.getBindingResult());
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
        log.error("handleBusinessException", e);
        final ErrorResponse response = ErrorResponse.of(e.getErrorCode());
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

    /** Exception */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(Exception e) {
        log.error("handleException", e);
        final ErrorResponse response = ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR);
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

}

BindingResult 를 넘겨주기 위해 e.getBindingResult() 으로 수정하였다

 

[ErrorResponse 코드]

@Getter
@NoArgsConstructor
public class ErrorResponse {

    private String message;
    private int status;
    private String code;
    private List<FieldError> errors = new ArrayList<>();

    private ErrorResponse(final ErrorCode code) {
        this.message = code.getMessage();
        this.status = code.getStatus();
        this.code = code.getCode();
    }

    public ErrorResponse(ErrorCode code, BindingResult result) {
        this.message = code.getMessage();
        this.status = code.getStatus();
        this.code = code.getCode();
        this.errors = result.getFieldErrors();
    }

    public static ErrorResponse of(final ErrorCode code) {
        return new ErrorResponse(code);
    }

    public static ErrorResponse of(ErrorCode errorCode, BindingResult bindingResult) {
        return new ErrorResponse(errorCode, bindingResult);
    }
}

GlobalExceptionHandler 에서 매개변수를 BindingResult 로 수정했기 때문에

생성자 또한 String message 지우고 BindingResult bindingResult 로 수정한다

그리고 getFieldErrors() 메소드로 리턴 된 List<FieldError> 를 ErrorResponse 멤버변수로 바꿔준다. 

 

 

[MethodArgumentNotValidException 요청 실패 결과]

 

아까 보다는 불필요한 정보들이 많이 사라졌지만, 그럼에도 클라이언트(사용자) 에게 응답 할 내용이라기에는

불필요한 정보들이 많이 포함되어있다. 

응답 내용으로 필요할 만한 내용으로는 defaultMessage, field, rejectedValue 으로도 충분해보인다.

이 3가지 멤버변수만을 가지는 클래스를 만드는 데, ErrorResponse 에서만 사용할 거기 때문에

static 내부 클래스로 클래스를 만들 것이다.

[ErrorResponse 코드]

@Getter
@NoArgsConstructor
public class ErrorResponse {

    private String message;
    private int status;
    private String code;
    private List<FieldError> errors = new ArrayList<>();

    private ErrorResponse(final ErrorCode code) {
        this.message = code.getMessage();
        this.status = code.getStatus();
        this.code = code.getCode();
    }

    public ErrorResponse(ErrorCode code, List<FieldError> errors) {
        this.message = code.getMessage();
        this.status = code.getStatus();
        this.code = code.getCode();
        this.errors = errors;
    }

    public static ErrorResponse of(final ErrorCode code) {
        return new ErrorResponse(code);
    }

    public static ErrorResponse of(ErrorCode errorCode, BindingResult bindingResult) {
        return new ErrorResponse(errorCode, FieldError.of(bindingResult));
    }

    @Getter
    private static class FieldError {

        // 필드명 (멤버변수)
        private String field;

        // request value (요청된 값 - 사용자가 보낸 값)
        private String value;

        // 오류 메시지 (이유)
        private String reason;

        private FieldError(String field, String value, String reason) {
            this.field = field;
            this.value = value;
            this.reason = reason;
        }

        private static List<FieldError> of(BindingResult bindingResult) {

            final List<org.springframework.validation.FieldError> errors = bindingResult.getFieldErrors();

            return errors.stream().map(error -> new FieldError(
                        error.getField(),
                        error.getRejectedValue() == null ? "" : error.getRejectedValue().toString(),
                        error.getDefaultMessage()
                    )).collect(Collectors.toList());
        }
    }
}

 

[MethodArgumentNotValidException 요청 실패 결과]

 


BindException

이 Exception 은 @ModelAttribute 으로 binding error 발생시 발생한다

@ModelAttribute 는 @RestController 가 아닌 Model 을 사용하는 @Controller 에서 사용한다

MethodArgumentNotValidException 처럼 BindiBindingResult.getFieldErrors() 를 사용한다

[GlobalExceptionHandler 코드]

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /** @Valid */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error("handleMethodArgumentNotValidException", e);
        final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE,e.getBindingResult());
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

    /** @ModelAttribute */
    @ExceptionHandler(BindException.class)
    protected ResponseEntity<ErrorResponse> handleBindException(BindException e) {
        log.error("handleBindException", e);
        final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult());
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
        log.error("handleBusinessException", e);
        final ErrorResponse response = ErrorResponse.of(e.getErrorCode());
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

    /** Exception */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(Exception e) {
        log.error("handleException", e);
        final ErrorResponse response = ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR);
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

}

 


MethodArgumentTypeMismatchException

이 Exception 은 enum 타입이 일치하지 않아 binding 못할 경우 발생한다

주로 @Param 어노테이션을 통해 Enum 타입을 받을 때 에러 발생

클래스의 필드로 사용하는 경우는 HttpMessageNotReadableException 을 발생한다

 

MissingServletRequestParameterException 은 해당 파라미터를 받아야 하는데

클라이언트에서 파리미터 전송을 안했을 경우 발생한다

단 @Param(require = false) 로 설정하면 exception 이 발생하지 않는다.

 

[MemberType 코드]

public enum MemberType {
    BRONZE_MEMBER("BRONZE", 0L),
    SILVER_MEMBER("SILVER", 25L),
    GOLD_MEMBER("GOLD", 50L);

    private final String type;
    private final Long level;

    MemberType(final String type, final Long level) {
        this.type = type;
        this.level = level;
    }

    public String getType() {
        return type;
    }

    public Long getLevel() {
        return level;
    }
}

 

[Member 코드]

@Entity
@NoArgsConstructor
@Getter
@Setter
@EqualsAndHashCode(of = "memberId")
@ToString
public class Member {

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

    @Size(max = 20, message = "0~20 글자사이만 가능합니다")
    @Column(name = "member_id", nullable = false, length = 20, unique = true)
    private String memberId;

    @Column(name = "password", nullable = false, length = 20)
    private String password;

    @Column(name = "point", nullable = false)
    private Long point;

    @Column(name = "member_type", nullable = false)
    private MemberType memberType; // << 이부분 추가됨

    @Builder
    private Member(String memberId, String password) {
        this.memberId = memberId;
        this.password = password;
    }
}

 

[MemberController 코드]

@RestController
@RequestMapping("/v1")
@RequiredArgsConstructor
@Slf4j
public class MemberController {

    private final MemberService memberService;

    /** 생성 */
    @PostMapping("/members")
    public ResponseEntity<?> create(@RequestBody @Valid Member resources) throws URISyntaxException {

        resources.setMemberType(MemberType.BRONZE_MEMBER); // << 이부분 추가

        log.info("resources -> {}", resources);

        Member member = memberService.create(resources);

        log.info("member -> {}", member);

        URI url = new URI(String.format("/members/%s", member.getId()));

        return ResponseEntity.created(url).body(member);
    }

    /** 생성 */
    @PostMapping("/members2")
    public ResponseEntity<?> create2(@RequestBody @Valid Member resources) throws URISyntaxException {

//        resources.setMemberType(MemberType.BRONZE_MEMBER); 

        log.info("resources -> {}", resources);

        Member member = memberService.create(resources);

        log.info("member -> {}", member);

        URI url = new URI(String.format("/members/%s", member.getId()));

        return ResponseEntity.created(url).body(member);
    }

    /** 조회 */
    @GetMapping("/members/{id}")
    public Member details(@PathVariable Long id) {
        return memberService.detail(id);
    }


    /** 리스트 */
    @GetMapping("/members")
    public List<Member> list() {
        return memberService.list();
    }

    /** 수정 */
    @PatchMapping("/members/{id}")
    public void update(@RequestBody Member resources, @PathVariable Long id) {
        log.info("resources -> {}", resources);
        log.info("id -> {}", id);

        memberService.update(resources, id);
    }

    /** enum 타입 수정 */
    @PatchMapping("/param/members/{id}")
    public void update(@RequestParam(value = "type", required = true) MemberType type) { // << 이부분 추가
        log.info("memberType -> {}", type); 
//        log.info("type -> {}", type.getType());
//        log.info("level -> {}", type.getLevel());
    }

    /** 삭제 */
    @DeleteMapping("/members/{id}")
    public void delete(@PathVariable Long id) {
        log.info("id -> {}", id);

        memberService.delete(id);
    }

}

 

[ErrorCode 코드]

@Getter
public enum ErrorCode {
    INVALID_INPUT_VALUE(400, "C002", "잘못된 요청"),
    INTERNAL_SERVER_ERROR(400, "C001", "서버 오류"),
    INVALID_TYPE_VALUE(400, "C005", "잘못된 타입") // << 이부분 추가
    ;

    private String message;
    private int status;
    private String code;

    ErrorCode(int status,  String code, String message) {
        this.status = status;
        this.code = code;
        this.message = message;
    }
}

 

[ErrorResponse 코드]

@Getter
@NoArgsConstructor
public class ErrorResponse {

    private String message;
    private int status;
    private String code;
    private List<FieldError> errors = new ArrayList<>();

    private ErrorResponse(final ErrorCode code) {
        this.message = code.getMessage();
        this.status = code.getStatus();
        this.code = code.getCode();
    }

    private ErrorResponse(ErrorCode code, List<FieldError> errors) {
        this.message = code.getMessage();
        this.status = code.getStatus();
        this.code = code.getCode();
        this.errors = errors;
    }


    public static ErrorResponse of(final ErrorCode code) {
        return new ErrorResponse(code);
    }

    public static ErrorResponse of(ErrorCode errorCode, BindingResult bindingResult) {
        return new ErrorResponse(errorCode, FieldError.of(bindingResult));
    }

    public static ErrorResponse of(MethodArgumentTypeMismatchException e) { // << 이부분
        final String value = e.getValue() == null ? "" : e.getValue().toString();
        final List<ErrorResponse.FieldError> errors = ErrorResponse.FieldError.of(e.getName(), value, e.getErrorCode());
        return new ErrorResponse(ErrorCode.INVALID_TYPE_VALUE, errors);
    }

    @Getter
    private static class FieldError {

        // 필드명 (멤버변수)
        private String field;

        // request value (요청된 값 - 사용자가 보낸 값)
        private String value;

        // 오류 메시지 (이유)
        private String reason;

        private FieldError(String field, String value, String reason) {
            this.field = field;
            this.value = value;
            this.reason = reason;
        }

        private static List<FieldError> of(BindingResult bindingResult) {

            final List<org.springframework.validation.FieldError> errors = bindingResult.getFieldErrors();

            return errors.stream().map(error -> new FieldError(
                        error.getField(),
                        error.getRejectedValue() == null ? "" : error.getRejectedValue().toString(),
                        error.getDefaultMessage()
                    )).collect(Collectors.toList());
        }

        public static List<FieldError> of(String name, String value, String errorCode) { // << 이부분
            return Collections.singletonList(new FieldError(name, value, errorCode));
        }
    }
}

e.getName() 은

Controller  에 @RequestParam(value = "type", required = true) 에 value 인 type 을 반환하고

e.getValue() 는

Object 타입이기 때문에 null 이면 "" 빈값 아니면 클라이언트에서 입력한 내용을 반환한다

e.getErrorCode() 는 해당 에러코드를 반환한다 (typeMismatch)

ErrorResponse 는 List<FiledError> 형태로 응답하기 때문에

FieldError 객체가 1개뿐이라도 List<FiledError> 형태로 반환한다

 

 

[MethodArgumentTypeMismatchException 으로 요청]

 

[GIF]

[HttpMessageNotReadableException]

memberType 은 MemberType 클래스이다.

그런데 "A" 라는 타입은 존재하지않기 때문에

exception 을 발생한다

 

[MethodArgumentTypeMismatchException]

@RequestParam 에서 GOLD_MEMBER 는 MemberType Enum 클래스에 존재하기 때문에 성공하고

AAAAA 는 존재하지 않기 때문에 exception 이 발생한다.

 

HttpMessageNotReadableException 은 json 방식이고

MethodArgumentTypeMismatchException 은 파라미터 방식으로 요청했다

 

[MissingServletRequestParameterException]

서버에서는 @RequestParam 으로 해당 파라미터를 꼭 요청받야한다

그 이유는 @RequestParam 에 require 속성의 기본값은 true 이기 때문에 

해당 파라미터는 필수로 받아야 한다.

 

?type= 이런식으로 값을 아무것도 보내지 않아도 오류는 나지 않는다.

심지어 MethodArgumentTypeMismatchException 타입 오류도 나지 않는다

하지만 DB에 insert 할 때에는 null 값이라 쿼리 exception 은 발생한다

 

클라이언트에서 해당 파라미터를 요청하지 않는 경우에는 이 exception 을 발생 시킨다.

@RequestParam 에 require 속성을 false 로 한다면, 이 오류는 발생하지 않는다

 


HttpRequestMethodNotSupportedException

이 exception 은 지원하지 않는 HTTP 메소드 인 경우 exception 을 발생시킨다. 

NotFoundException 과는 다릅니다. 주로 @PathVariable 의 url 부분을 안적었을 때 발생한다.

 

[GlobalExceptionHandler 코드]

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /** @Valid */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error("handleMethodArgumentNotValidException", e);
        final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE,e.getBindingResult());
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

    /** @ModelAttribute */
    @ExceptionHandler(BindException.class)
    protected ResponseEntity<ErrorResponse> handleBindException(BindException e) {
        log.error("handleBindException", e);
        final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult());
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

    /** enum type */
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    protected ResponseEntity<ErrorResponse> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
        log.error("handleMethodArgumentTypeMismatchException", e);
        final ErrorResponse response = ErrorResponse.of(e);
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

    /** @PathVariable */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class) // << 이 부분 추가
    protected ResponseEntity<ErrorResponse> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
        log.error("handleHttpRequestMethodNotSupportedException", e);
        final ErrorResponse response = ErrorResponse.of(ErrorCode.METHOD_NOT_ALLOWED);
        return new ResponseEntity<>(response, HttpStatus.METHOD_NOT_ALLOWED);
    }

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
        log.error("handleBusinessException", e);
        final ErrorResponse response = ErrorResponse.of(e.getErrorCode());
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

    /** Exception */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(Exception e) {
        log.error("handleException", e);
        final ErrorResponse response = ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR);
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

}

 

[ErrorCode 코드]

@Getter
public enum ErrorCode {
    INVALID_INPUT_VALUE(400, "C002", "잘못된 요청"),
    INTERNAL_SERVER_ERROR(400, "C001", "서버 오류"),
    INVALID_TYPE_VALUE(400, "C005", "잘못된 타입"),
    METHOD_NOT_ALLOWED(405, "C006", " 잘못된 요청") // << 이 부분 추가
    ;

    private String message;
    private int status;
    private String code;

    ErrorCode(int status,  String code, String message) {
        this.status = status;
        this.code = code;
        this.message = message;
    }
}

 

[ HttpRequestMethodNotSupportedException 요청 실패 ]

 


AccessDeniedException

이  exception 은 스프링 Security 에서 인증 실패 시 발생한다

 


참고 HttpMessageNotWritableException

json 으로 변환 시 해당 클래스에 getter 메소드가 없는 경우 발생하는 exception