Memories in SeoK

기억하고 싶은 것들, 기억해야 하는 것들

개발/자바 Java

따라가며 만들기 + 마이그레이션 연습 (Spring Boot, AWS)

Seo K 2024. 6. 16. 04:57

기억을 되새길 겸, 블로그 콘텐츠도 얻을 겸 몇 년 전에 작성된 책에 (이동욱 저 - 스프링 부트와 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 패키지로 변경해야 한다
대부분 최상위 이름만 바꿔주면 된다

 

[Java 입문] JRE? JDK? 기초 용어 정리와 다운로드

용어 정리 다양한 JDK들 Java 에디션용어 정리 JVM (Java Virtual Machine): Java 프로그램을 실행할 때 사용되는 가상 머신 OS에 무관하게 같은 동작을 보장하는 JAVA의 특징은 JVM이 있기에 가능한 것이다. cl

mem-in-seok.tistory.com

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"가 함께 생성된다고 한다

빌드시 plain.jar 생성 방지

 

빌드시 plain.jar 생성 방지

spring boot 2.5 이상부터 생성된다고함 -plain 이 붙은 jar 파일은 plain archive 라고하며 애플리케이션 실행에 필요한 모든 의존성을 포함하지 않고, 작성된 소스코드의 클래스 파일과 리소스 파일만 포

bkjeon1614.tistory.com

더보기
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 사용에 따른 변경

 

[Java 중급] deprecated Spring Security Configuration

WebSecurityConfigurerAdapterconfigure 메서드를 Overriding하여 웹 보안 설정을 구성할 때 사용했던 기본 클래스(보안 설정 예시: 특정 URL에 대한 접근 권한 설정, 폼 로그인, HTTP Basic 인증 등) Spring Security 5.4부터

mem-in-seok.tistory.com

- 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)

 

[Java] Spring Boot Test - IllegalArgumentException: Failed to find servlet [] in the servlet context

Spring Boot 3.2.5에서 (Spring Security 6.2.4) random port로 Mock mvc test 진행 시 IllegalArgumentException 발생java.lang.IllegalArgumentException : Failed to find servlet [] in the servlet context main 쪽에 구성한 Web Security Configuration

mem-in-seok.tistory.com

- 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();

 


 

[Ubuntu] 따라하기 + 배포 연습 (Spring Boot)

[ 이동욱 저 - 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 ] 책의 예제를 최신 프레임워크와 라이브러리로 대체하여 따라 하기 중 만들기에 (프로젝트 작성에) 집중한 이전 글에 이어서 작성합

mem-in-seok.tistory.com