기억을 되새길 겸, 블로그 콘텐츠도 얻을 겸 몇 년 전에 작성된 책에 (이동욱 저 - 스프링 부트와 AWS로 혼자 구현하는 웹 서비스) 있는 예제 프로젝트를 최신 프레임워크와 라이브러리로 대체해서 따라 해보고 있다
(cf. https://github.com/Seo-Kim/example-book-aws-springboot)
앞으로 이 글을 계속 수정해 가며 작성해 나갈 예정이다 (2024.06.15.) 프로젝트 작성까지만 적고 이후 프로젝트 배포부터는 다른 글에 이어 적기로 했다
* 궁금한 점, 요청 사항, 조언, 지적 등을 댓글이나 GitHub 이슈에 남겨 주시면 감사하겠습니다
Temurin OpenJDK 21 (Compile Version: Java 17)
Spring Boot 3.2.5
-- Spring 6.1.6
-- Tomcat Embed 10.1.20
-- Hibernate 6.4.4.Final
-- Lombok 1.18.32
-- H2 2.2.224
-- jMustache 1.15
-- Spring Security (OAuth2 Client) 6.2.4
JUnit 5.10
Gradle 8.5
jQuery 3.7.1
Bootstrap 5.3.3
Java Version 관련
* javax 패키지 변경 :: jakarta
Java 버전을 올렸기 때문에 javax 패키지는 jakarta 패키지로 변경해야 한다
대부분 최상위 이름만 바꿔주면 된다
VM option
* 파일 인코딩 설정 추가 :: Dfile.encoding
[ Settings > File Encoding ]에서 모두 UTF-8로 변경했기 때문에 로그에서 한글을 정상 출력하려면 JVM 옵션도 함께 변경해줘야 한다 (기본값은 x-windows-949 (EUC-KR))
[ Menu > Help > Edit Custom VM Options ] > idea64.exe.vmoptions 파일에 추가
-Dfile.encoding=UTF-8
코드로 VM 인코딩 확인하는 방법
System.out.println( java.nio.charset.Charset.defaultCharset().displayName() );
Gradle 관련
* JCenter 저장소 삭제
악성 코드가 포함된 라이브러리가 배포되는 문제로 2021년 5월에 운영사 JFrog는 저장소 배포 플랫폼인 Bintray의 서비스 중단을 결정했음 (JFrog 공지 (영문))
* buildscript 불필요 :: plugins
buildscript: 프로젝트의 빌드 스크립트를 실행하는 데 필요한 의존성을 구성하고 플러그인을 관리하는 등 설정을 정의하는 블록 (실제 코드 빌드에는 관여하지 않는다)
ext 블록으로 전역 변수 설정, dependencies 블록으로 의존성 정의
실제 코드 빌드에는 관여하지 않으므로, 프로젝트의 실행 환경과 빌드 환경을 명확히 구분지어서 관리할 수 있다는 장점이 있다. 특정 플러그인이나 도구들은 (eg. Spring Boot Gradle 플러그인) 프로젝트 빌드 과정에만 관여하므로 별도로 분리하여 빌드 환경을 일관되게 유지하는 등 관리하기에 용이하다는 것이다. 빌드 환경만 따로 재사용하거나 확장하는 등 유연성도 가질 수 있다.
apply plugin 기능에 의존성 관리, 적용 순서 지정까지 가능한 plugins 블록만 사용함으로, 최종적으로 buildscript 블록을 제거하는 것이 Gradle 팀의 목표라는 이야기가 있는데 공식 출처는 못 찾았다
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.5'
id 'io.spring.dependency-management' version '1.1.4'
}
* plain.jar 생성 방지 :: jar enabled
Spring Boot 2.5 이후로 빌드 시 "[빌드명].jar"와 함께 "[빌드명]-plain.jar"가 함께 생성된다고 한다
jar {
enabled = false
}
* compile 메서드 대체 :: implementation
기본적으로는 "compile" 문구를 "implementation"으로 변경
compile '...' > implementation '...'
lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
junit 5
testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
h2 database
runtimeOnly 'com.h2database:h2'
Git 관련
* ignore 플러그인 설치 불필요
IntelliJ에서 프로젝트를 생성하는 시점에 gitignore 파일을 기본적으로 생성해 줄 뿐 아니라 이미 IDE 별 설정 파일들을 내용에 포함시켜 두었다
application.properties 관련
* DB 버전별 방언 deprecate
(spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect 대체)
Spring Boot가 연결되어 있는 데이터베이스에 알맞게 자동으로 지정하므로, 수동으로 dialect를 설정하지 않는 것을 권장하고 있다. 또한 구현체를 Hibernate에서 바꾸게 될 가능성도 염두하여 Hibernate 설정을 직접 사용하기보다는 Spring 설정을 사용하는 것을 권장한다.
다음과 같이 변경하면 적용 가능한 최저 버전인 MySQL 8로 동작한다
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
spring.datasource.url=jdbc:h2:mem:testdb;MODE=MYSQL
* Server (Tomcat) HTTP 응답 인코딩 설정 :: HTTP 응답 인코딩 강제 설정
Mustache 작성 시 meta charset 속성을 입력했음에도 불구하고 브라우저에서 한글이 비정상 출력되는 문제가 있다
책에서 언급되지 않은 것은 Spring Boot 버전이 달라지면서 새로 생긴 문제이기 때문이 아닐까 추측한다
File Encoding Settings, JVM Option, Mustache charset 설정은 현 상황과 무관하다
Server HTTP 인코딩 설정이 별도로 있는데, 기본값이 UTF-8로 되어 있지만 강제 설정을 해줘야 작동하는 건지, 아니면 Spring Boot 버그인 건지 모르겠다
HTTP 응답에 대해서만 설정해 주면 된다
(cf. Spring Boot 3.2.5 Ref Doc (영문))
server.servlet.encoding.force-response=true
간혹 spring.http.encoding.force 설정을 해결책으로 제시하는 글이 보이는데, Spring Boot 2.3 버전부터 사용할 수 없는 설정이다
Spring Security 관련
* Security 6.2.4 사용에 따른 변경
- WebSecurityConfigurerAdapter deprecated :: SecurityFilterChain
SecurityContext.and() 메소드로 연결하는 대신 각 설정을 별도로 구성 (독립성, 가독성 증가)
- authorizeRequests() deprecated :: authorizeHttpRequests
- antMatchers() deprecated :: requestMatchers
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.web.SecurityFilterChain;
// ...
@EnableWebSecurity
@Configuration
public class SecurityConfig {
// ...
@Bean
public SecurityFilterChain securityFilterChain( HttpSecurity http ) throws Exception {
http
.csrf( httpSecurityCsrfConfigurer -> httpSecurityCsrfConfigurer.disable() )
.headers( httpSecurityHeadersConfigurer -> httpSecurityHeadersConfigurer
.frameOptions( frameOptionsConfig -> frameOptionsConfig.disable() )
)
.authorizeHttpRequests( authorizationManagerRequestMatcherRegistry -> authorizationManagerRequestMatcherRegistry
.requestMatchers( "/", "/css/**", "/images/**", "/js/**", "/h2-console/**" ).permitAll()
.requestMatchers( "/api/v1/**" ).hasRole( Role.USER.name() )
.anyRequest().authenticated()
)
.logout( logout -> logout.logoutSuccessUrl( "/" ) )
.oauth2Login( httpSecurityOAuth2LoginConfigurer -> httpSecurityOAuth2LoginConfigurer
.userInfoEndpoint( userInfoEndpointConfig -> userInfoEndpointConfig
.userService( customOAuth2UserService )
)
);
return http.build();
}
}
Test 관련
* JUnit5 사용에 따른 변경
cf. JUnit 4에서 JUnit 5로 마이그레이션 | IntelliJ IDEA 블로그 (영문)
- junit.runner, junit4의 @RunWith( SpringRunner.class ) 대체 :: ExtendWith
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith( SpringExtension.class )
- assertj의 assertThat( [실제값] ).isEqualTo( [예상값] ) 대체 :: assertEquals
import static org.junit.jupiter.api.Assertions.assertEquals;
assertEquals( [예상값], [실제값] );
- org.junit.@After 대체 :: AfterEach
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.AfterAll;
@AfterEach // 각 Test 메서드 끝날 때마다 실행
public void myFuncEvery() {}
@AfterAll // 모든 Test 메서드 끝나면 한번 실행 (static 정의)
public static void myFuncOnce() {}
* src/test/.../application.properties 생성 불필요 (관련 에러 발생 없음)
버전 올라가면서 src/main에 있는 properties 파일들을 불러오도록 수정된 것으로 추측
* Spring Security 사용에 따른 변경
- Security 6.2 이상에서 Random Port 사용하여 Test 시 Servlet 못 찾는 문제 (IllegalArgumentException)
- MockMvcBuilders 사용 불필요, MockMvc 자동 주입 :: @AutoConfigureMockMvc
- MediaType.APPLICATION_JSON_UTF8 deprecated :: MediaType.APPLICATION_JSON
Spring Framework 5.2부터 (Spring Boot 2.2) deprecated, UTF-8이 기본으로 채택됨
* HTTP Response Encoding :: getContentAsString( StandardCharsets.UTF_8 )
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.security.test.context.support.WithMockUser;
// ...
@AutoConfigureMockMvc
@WithMockUser( roles="USER" )
class PostApiControllerTest {
// ...
/* @AutoConfigureMockMvc로 대체
@Autowired
private WebApplicationContext context;*/
@Autowired
private MockMvc mvc;
@Autowired
private ObjectMapper objectMapper;
/* @AutoConfigureMockMvc로 대체, 자동 주입 사용
@BeforeEach
void setup() {
mvc = MockMvcBuilders
.webAppContextSetup( context )
.apply( SecurityMockMvcConfigurers.springSecurity() )
.build();
}*/
// Spring Security 6.x random port Test occur IllegalArgumentException
private MockHttpServletRequest makeRequestMacherServlet( MockHttpServletRequest request ) {
String matchValue = request.getRequestURI();
String servlet = "dispatcherServlet";
String pattern = request.getServletContext().getServletRegistration( servlet ).getMappings().iterator().next();
HttpServletMapping mapping = new MockHttpServletMapping( matchValue, pattern, servlet, MappingMatch.PATH );
request.setHttpServletMapping( mapping );
return request;
}
@Test
void 등록_정상() throws Exception {
// ...
/* Security 적용으로 MockMvc로 대체
ResponseEntity< Long > responseEntity = restTemplate.postForEntity( url, requestSaveDto, Long.class );
assertEquals( HttpStatus.OK, responseEntity.getStatusCode() );*/
String resString = mvc.perform( MockMvcRequestBuilders
.post( url )
.contentType( MediaType.APPLICATION_JSON )
.content( objectMapper.writeValueAsString( requestSaveDto ) )
// Security 6.x bug > Servlet 직접 지정
.with( request -> makeRequestMacherServlet( request ) )
)
.andExpect( MockMvcResultMatchers.status().isOk() )
.andReturn()
.getResponse().getContentAsString( StandardCharsets.UTF_8 );
//Long savedId = responseEntity.getBody();
Long savedId = Long.valueOf( resString );
// ...
/* Security 적용으로 MockMvc로 대체
ResponseEntity< PostDto.Select > responseSelectEntity = restTemplate.getForEntity( url + "/" + savedId, PostDto.Select.class );
PostDto.Select selectDto = responseSelectEntity.getBody();*/
resString = mvc.perform( ... ;
PostDto.Select selectDto = objectMapper.readValue( resString, PostDto.Select.class );
// ...
}
// ...
}
MediaType에 다른 charset을 적용하는 방법
import java.nio.charset.Charset;
// 생성 시 적용
MediaType mediaType = new MediaType("application", "json", Charset.forName("UTF-16"));
// 일회성 적용
MediaType.APPLICATION_JSON.withCharset("UTF-16");
Domain, Repository 관련
* Entity 이름, Field 이름에 예약어 사용 기피 권장
MySQL 시스템 테이블 중에 'USER' 존재하므로 Entity 이름을 변형 (Users 등)
* 도메인 객체에 equals 정의 :: @EqualsAndHashCode
개인적으로 특별한 이유가 없다면 PK로 동일 객체인지 확인하도록 설정하는 편이다
equals 메서드를 정의하면 객체 자체를 바로 비교할 수 있어 편리하기 때문이다
of 속성으로 확인 대상 필드를 정의, exclude 속성으로 확인 제외 필드를 정의하는 방식도 가능
// Entity
import lombok.EqualsAndHashCode;
@EqualsAndHashCode( of="id" )
@EqualsAndHashCode( exclude={ "createDate", "updateDate" } )
class ...
// Test 등
...
assertEquals( domainExpect, domainActual );
Javascript 관련
* index.js 시작 부분에서 개인적으로 갖게 된 의문이 있어, 우선 다르게 작업하고 문제가 생기면 고쳐가기로 했다
- 변수명 main :: 파일명과 일치
- 공통으로 사용될 스크립트와 특정 화면의 스크립트 구분 필요
1. 저장 관련 함수를 정의했는데 왜 index.js인가?>> 파일명을 post-save.js로 하고, 변수명도 post_save 또는 postSave로 하는 것이 맞을 것 같다
>> 등록, 수정 버튼 관련 기능은 공통으로 관리할 수 있을 것 같다. common.js로 만들고 변수명을 common으로 변경했다.
2. 특정 화면에서 동작하는 함수들을 공통 레이아웃인 footer에 넣는 이유가 무엇인가?
화면을 열 때마다 관련 없는 스크립트들을 계속 불러오는 걸로밖엔 안 보이는데 무슨 장점이 있는 것인지 모르겠다>> footer가 아닌 post-save 화면에 붙이기로 했다
>> 공통으로 사용할 common.js는 footer에 붙였다. 위 의문에 대한 명확한 답을 찾기 전까지는 책에서 footer에 붙이더라도 나는 개별 화면에 붙일 것이다.
// common.js
const common = {
init: function() {
const _this = this;
$( "#btn-save" ).on( "click", function() {
_this.save();
} );
//...
},
save: function() {
//...
},
//...
};
common.init();
'개발 > 자바 Java' 카테고리의 다른 글
[Ubuntu] 따라하기 + 배포 연습 (Spring Boot) (0) | 2024.06.16 |
---|---|
[Java] Spring Boot Test - IllegalArgumentException: Failed to find servlet [] in the servlet context (0) | 2024.06.09 |
[Java 중급] deprecated Spring Security Configuration (0) | 2024.06.06 |
[IntelliJ] 인텔리제이 초기 설정 / 옵션 (개인 기록용) (1) | 2024.05.27 |
[jQuery] 코드 블럭 복사하기 - (문제 해결) 플러그인과 함께 사용하기 (0) | 2024.05.18 |
[jQuery] 코드 블럭 복사하기 - (문제 인지) 플러그인들과의 충돌 (2) | 2024.05.17 |