02. 스프링부트 (Spring Boot) Profile, JPA Naming, Exception 전략
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
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 ] 방법으로 작성한 경우 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 로 빌드 시에도 설정이 가능하다
[ Hikari Connection Pool ]
[ ddl-auto ]
방식 | 설명 |
create | SessionFactory 가 올라갈 때 테이블을 지우고 새로 만듬 - 데이터 삭제 |
create-drop | SessionFactory 가 올라 갈 때 테이블을 생성, SessionFactory 가 내려가면 테이블 삭제 - 데이터 삭제 |
update | SessionFactory가 올라갈 때 Object를 검사하여 테이블을 alter 시킨다. - 데이터는 유지 |
validate | update처럼 Object를 검사하지만, 스키마는 아무것도 건드리지 않고, Object와 스키마의 정보가 다르다면 에러를 발생시킨다. - 데이터 유지 |
[open-in-view 참고 바람]
[log back]
[기타 참고한 사이트]
#2. JPA Naming 전략
Naming | 설명 |
SpringPhysicalNamingStrategy [기본값] | camel case를 underscore 형태로 변경 |
PhysicalNamingStrategyStandardImpl | 변수 이름을 그대로 사용 |
SpringPhysicalNamingStrategy
이 방식은 memberId 인 경우 DB 컬럼명이 memberId 가 아닌 member_id 로 된다
PhysicalNamingStrategyStandardImpl
이 방식은 memberId 인 경우 DB 컬럼명이 memberId 로 된다
[자세한 내용은 아래 참고]
#3. Exception 전략
참고한 블로그
[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 는 애플리케이션이 커질 수록 증가하게 되는 데, 결국
GlobalExceptionHandler 에 MemberIdDuplicateException 과 같은 핸들링을 여러개 추가해야 하고,
ErrorResponse의 매개변수 인 status 와 code 또한 계속 적어주어야 한다.
이런점을 보완하기 위해 message, status, code 매개변수에 대한 책임들을 exception 에게 주도록 해야 한다.
이를 위해 ErrorCode Enum 클래스, InvalidValueException 클래스, EntityNotFoundException 클래스,
BusinessException 클래스를 만들 것이다.
비지니스 상에서 일어날 수 있는 오류들(BusinessException)은 크게 2가지로 나뉜다.
첫 번째는 잘못된 요청(InvalidValueException) 과 두 번째는 없는 데이터에 대한 요청(EntityNotFoundException) 이다
MemberIdDuplicateException 는 존재하지만 비지니스적인 이유로 동일한 id로는 가입할 수 없도록 막은 것이기 때문에
잘못된 요청 (InvalidValueException)에 포함된다. (상속)
예시 - 커스텀 Exception 구조 (BusinessException)
[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 메소드를 이용해 생성자를 생성한 이유는 아래 사이트 참고바람
[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 | 그 외 오류들 |
[상태 코드 참고]
MethodArgumentNotValidException
이 Exception 은 유효성 검사를 할 때 발생하는 exception 이다
유효성 검사에 실패하면 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() 만 넘긴다면 불필요한 내용도 응답이 된다.
여기서 필요한 정보만 가져오기 위해 MethodArgumentNotValidException 의 BindingResult.getFieldErrors() 를
사용할 것이다. 이 메소드를 사용하면 List<FieldError> 를 리턴해준다
FieldError 에는 message, field, code, objectName 등 자세한 내용을 알 수 있다.
[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
'10. Spring > BOOT' 카테고리의 다른 글
스프링 부트 MVC - Handler Interceptor (0) | 2022.04.14 |
---|---|
11. 스프링 부트 (Spring Boot 2) & 뷰 (Vue 2)- 환경 구축하기 (0) | 2021.03.26 |
10. 스프링부트 (Spring Boot 2.4.3) - javax Transactional과 spring Transactional (0) | 2021.03.14 |
09. 스프링 부트 (Spring Boot) - thymeleaf 페이징 처리 Pageable [미완성] (0) | 2020.11.19 |
06. 스프링 부트 (Spring Boot) - 자바 메일 센더 (Java Mail Sender) (0) | 2020.08.29 |
댓글
이 글 공유하기
다른 글
-
11. 스프링 부트 (Spring Boot 2) & 뷰 (Vue 2)- 환경 구축하기
11. 스프링 부트 (Spring Boot 2) & 뷰 (Vue 2)- 환경 구축하기
2021.03.26 -
10. 스프링부트 (Spring Boot 2.4.3) - javax Transactional과 spring Transactional
10. 스프링부트 (Spring Boot 2.4.3) - javax Transactional과 spring Transactional
2021.03.14 -
09. 스프링 부트 (Spring Boot) - thymeleaf 페이징 처리 Pageable [미완성]
09. 스프링 부트 (Spring Boot) - thymeleaf 페이징 처리 Pageable [미완성]
2020.11.19 -
06. 스프링 부트 (Spring Boot) - 자바 메일 센더 (Java Mail Sender)
06. 스프링 부트 (Spring Boot) - 자바 메일 센더 (Java Mail Sender)
2020.08.29