초록 스터디 2주차

송선권·2023년 11월 28일
2

초록 스터디

목록 보기
2/5
post-thumbnail

2주차 스터디 진행 상황

1단계 완료, 2단계 완료

1단계 학습 주제: 웹 요청과 응답을 처리하기 위한 Spring MVC의 기능
2단계 학습 주제: 데이터베이스 접근을 위한 Spring JDBC

1단계

스터디 진행 과정

Cannot resolve method is()

"1단계 - 예약 조회"의 테스트 코드를 추가하는 과정에서 위와 같이 is() 메서드가 인식되지 않는 현상이 발생했다. IntelliJ가 제시하는 import 명단에도 적절해보이는 라이브러리가 표시되지 않았고, 웹서핑을 해보아도 RestAssured 라이브러리의 .body() 메서드에 함께 쓰인다는 내용 뿐 is() 메서드의 출처가 명시된 페이지는 찾을 수 없었다.

그렇게 포기하고 스터디 커뮤니티에 질문글을 작성하던 중, 마지막으로 GPT에게 질문이라도 해보자는 생각이 들었다. GPT는 org.hamcrest.Matchers 라이브러리에 있는 is() 메서드를 import하면 해결할 수 있다는 답변을 내놓았고, 실제로 그렇게 import하니 문제가 해결되었다.

아직 테스트 코드에 관한 지식과 웹서핑 실력이 많이 부족하다는 것을 느낄 수 있는 시간이었다...

AtomicLong

AtomicLongLong 자료형을 가지는 Wrapper 클래스이다. Long이라는 래퍼 클래스가 기존에 존재하는데 왜 AtomicLong이라는 새로운 클래스가 필요한 것일까? 이는 멀티 스레드 환경과 관련이 있다.

synchronized

멀티 스레드 환경에서는 하나의 자원에 대해 여러 스레드에서 동시에 접근하는 일이 일어나는데, 이 때 동시성 환경에서의 값의 불일치 문제가 발생할 수 있다.

이를 막기 위해 스레드 동기화라는 작업을 진행하기도 하는데, 이는 멀티스레드 환경에서 여러 스레드가 하나의 공유자원에 동시에 접근하지 못하도록 막는 것(lock) 을 말한다. 이러한 공유 데이터, 즉 동기화가 필요한 부분을 임계 영역(Critical Section) 이라고 하며, 여기에 synchronized 키워드를 붙여 스레드 동기화를 수행하게 할 수 있다.

하지만 이렇게 되면 하나의 스레드가 공유 자원에 접근 중인 경우 다른 스레드는 해당 자원에 접근할 수 없게 막혀(lock) 자신의 순서를 기다리게 되는데, 이는 곧 성능적인 문제로 이어진다. 이런 문제를 해결하기 위해 논블로킹(non-blocking) 알고리즘 중 하나인 CAS(CompareAndSwap)을 적용할 수 있다.

자바 - Synchronized 스레드 동기화 개념 및 사용예제

CAS(CompareAndSwap) 알고리즘

CAS 알고리즘은 "현재 스레드에 저장된 값과 메인 메모리에 저장된 값을 비교"하여 일치할 경우 새로운 값으로 교체하고, 일치하지 않는다면 실패 후 재시도한다. 이 과정을 거치면 가시성 문제가 해결된다.

가시성 문제
멀티 스레드 환경에서 각 CPU는 자신의 캐시 메모리에서 값을 참조한다. 하지만 메인 메모리 값과 이 캐시 메모리에서의 값이 다른 경우가 있는데, 이를 가시성 문제라고 한다.

[운영체제] Atomic연산, CAS(CompareAndSwap)에 대하여, ABA문제

그래서 AtomicLong은 무엇인지?

AtomicLong은 멀티 스레드 환경에서 값의 불일치 문제 없이 안전하게(thread-safe) 연산을 수행할 수 있도록 지원하는 Long에 대한 Wrapper 클래스이다. synchronized와 달리 CAS 알고리즘을 적용하여 연산 과정을 최적화하였다.

추가로 JAVA에서는 AtomicLong뿐만 아니라 AtomicInteger, AtomicBoolean, AtomicReference를 제공한다고 한다. 이러한 Atomic 클래스에 저장된 값을 수정하기 위해서는 별도로 제공되는 메서드를 사용해야만 한다.

Long 과 AtomicLong은 어떤 차이가 있을까?

@ExceptionHandler

@ExceptionHandler는 Controller 단에서 발생하는 예외를 잡아와서 메서드로 쉽게 처리할 수 있도록 도와주는 어노테이션이다. @ExceptionHandler를 사용할 때 처리할 예외를 함께 명시하지 않으면 모든 예외를 잡기 때문에 처리할 예외를 꼭 명시해주는 것이 좋다.

사용법

  1. @ControllerAdvice를 붙인 advice 클래스를 만들고 그 안에서 @ExceptionHandler 메서드를 작성한다. 이는 Application 전역(Global)에서 일어나는 예외를 잡을 때 사용한다.
  2. 특정 컨트롤러에 @ExceptionHandler 메서드를 작성한다.

우선 순위

기본적으로는 @ControllerAdvice 클래스에서 예외를 잡아 처리한다. 하지만 예외가 발생한 Controller 내에 그에 대한 @ExceptionHandler 메서드가 존재한다면 그 곳에서 예외 처리를 진행한다.

[스프링부트] @ExceptionHandler를 통한 예외처리
Spring boot 공부 3 - 예외 처리 ControllerAdvice와 @ExceptionHandler


2단계

스터디 진행 과정

데이터베이스 초기 데이터 삽입

스프링 부트에서는 Spring JDBC의 초기 데이터(DataSource) 세팅 기능을 제공한다. 애플리케이션 로딩 시 src/main/resources에 위치한 schema.sqldata.sql 파일을 기반으로 초기 데이터를 설정한다.

schema.sql

DDL(Data Definition Language, 데이터 정의어) 를 작성하는 파일
1. 데이터베이스 스키마(구조와 구성요소)를 정의한다.
2. 테이블, 뷰, 인덱스 등을 정의한다.

data.sql

DML(Data Manipulation Language, 데이터 조작어) 를 작성하는 파일
1. 데이터베이스에 삽입할 초기 데이터를 정의한다.
2. 데이터베이스를 초기화하고 초기 상태를 설정한다.

실행 순서

schema.sql이 먼저 실행되고 data.sql이 이후에 실행된다.

DataSourceInitializer

스프링에서는 DataSourceInitializer 빈을 사용하여 schema.sqldata.sql 파일을 실행한다. DataSourceInitializer는 데이터베이스의 초기화를 담당하는 인터페이스로, 빈으로 등록되어 있으면 자동으로 실행된다. 해당 파일에서 data-sourceschema.sql 파일의 위치를 지정할 수 있다.

[Spring/DB] Spring에서 schema.sql과 data.sql 파일은 어떻게 다르고 어떤 순서로 실행될까?
schema.sql data.sql - velog

SimpleJdbcInsert

JdbcTemplate에서는 Insert 시 별도의 반환이 이루어지지 않는다. 삽입한 데이터의 id 값을 얻기 위해서는 삽입한 데이터를 기준으로 다시 조회를 해야 한다. 과정도 복잡하고 리소스도 많이 잡아먹기 때문에 이를 해결하기 위한 방안이 몇 가지 존재하는데, 그 중 하나가 SimpleJdbcInsert이다.

사실 KeyHolder라는 기능을 사용해도 동일한 결과를 낼 수 있지만, 코드가 직관적이지 못하기 때문에(복잡하다) 훨씬 직관적인 SimpleJdbcInsert를 사용하기로 했다.

private final SimpleJdbcInsert simpleJdbcInsert;  
  
public ReservationDao(DataSource dataSource) {  
    this.simpleJdbcInsert = new SimpleJdbcInsert(dataSource)  
        .withTableName("reservation") // 테이블명
        .usingGeneratedKeyColumns("id"); // 반환할 칼럼명
}

SimpleJdbcInsert를 사용하기 위해서는 SimpleJdbcInsert의 생성자에 DataSource를 넣어줘야 하는데, 생성자 주입을 통해 진행할 수 있다. 이후에는 Insert 이후 반환받을 값에 대한 정보(테이블명, 칼럼명 등)를 등록한다.

public Long saveReservation(Reservation reservation) {  
    validateAddReservation(reservation);  
    var parameterSource = new BeanPropertySqlParameterSource(reservation);  
    Number id = simpleJdbcInsert.executeAndReturnKey(parameterSource);  
    reservation.setId(id.longValue());  
    return id.longValue();  
}

이후는 간단하다. SimpleJdbcInsert.executeAndReturnKey(SqlParameterSource)에 파라미터 정보를 넣고 메서드를 호출하면 Insert 후 삽입된 데이터의 특정 칼럼 값이 반환된다.

DB Insert 시 자동생성된 id 를 알아내기
SimpleJdbcInsert를 통한 쉬운 Insert
Class SimpleJdbcInsert (Spring Framework 6.1.1 API)

BeanPropertyRowMapperBeanPropertySqlParameterSource

BeanPropertyRowMapper는 Bean 객체를 기반으로 RowMapper를 생성한다.

BeanPropertySqlParameterSource는 Bean 객체를 기반으로 SqlParameterSource를 생성한다.

두 클래스는 각각 RowMapperSqlParameterSource를 알아서 생성해주기 때문에 JdbcTemplate이나 SimpleJdbcInsert를 사용할 때 VO 클래스 정보를 일일이 매핑시키지 않아도 되서 편리하다. 하지만 알아서 매핑해주는 만큼 해당 클래스의 필드 목록이 반드시 일치해야 한다.

Spring Jdbc Template 실습 정리

query()execute()update()의 차이

JdbcTemplate에는 쿼리를 날리는 방법으로 query()execute(), 그리고 update() 메서드를 지원한다. 셋은 메서드가 구분된 만큼 용도가 나뉘어 사용된다.

query()

query()는 보통 SELECT와 같이 데이터베이스에서 데이터를 검색하여 결과 집합을 반환하는 쿼리에 사용된다. 결과 집합은 객체 목록으로 반환되기 때문에 RowMapper 또는 ResultSetExtractor를 통해 결과 집합의 행에 매핑해주어야 한다.

execute()update()

execute()INSERT, UPDATE, DELETE 를 포함한 모든 SQL문을 실행할 때 주로 사용한다. 즉, DDL을 작성할 때 사용한다고 볼 수 있다. PreparedStatementCreatorStatementCallback를 사용할 수 있도록 제공해주고, 이를 수정하여 개발자가 원하는 결과를 만들 수 있다.

update()INSERT, UPDATE, DELETE와 같은 데이터 수정에 관한 쿼리를 날릴 때 주로 사용된다. 즉, DML을 작성할 때 사용한다고 볼 수 있다. 반환값으로는 SQL 쿼리 실행으로 인해 영향을 받은 행 수를 반환한다.

위에서 update()는 DML 외의 쿼리 수행이 불가능한 것처럼 이야기했지만 실제로는 가능하다. 그래서 execute()update()는 동작 자체는 거의 동일하고 실제로 혼용하여 사용하기도 한다. 하지만 세부적으로 들어가면 차이가 있기에 그 차이를 알고 용도에 맞게 사용하는 것이 좋아 보인다.

Lombok 사용 시 annotation processing 옵션을 켜야 하는 이유

Lombok을 사용하려고 할 때마다 우리는 Enable annotation processing 옵션을 켜야 한다. 별 생각 없이 해당 옵션을 키러 가다가 문득 이 옵션을 켜야 동작하는 이유가 궁금해졌다. 평소에는 그저 Lombok이 어노테이션 기반으로 코드를 생성해주기 때문에 켜야 한다고만 알고 있었지만 조금 더 세부적으로 알고 싶어졌다.

javac(자바 컴파일러)는 소스코드를 파싱하여 AST(Abstract Syntax Tree)를 생성하고 그 내용을 기반으로 Byte code를 작성하여 프로그램을 실행시킨다. Lombok은 이 과정에 관여하여 자신의 기능을 수행할 수 있다.

컴파일 단계에서 javac가 AST를 생성하면 Lombok은 Annotation Processer를 통해 AST를 조작한다. 기존에 없던 코드를 어노테이션에 정의된 동작을 수행하면서 생성하는 것이다. 정확히는 생성된 AST를 조작하여 원래 있던 코드인 것처럼 수정한다. 이후 javac는 Lombok에 의해 조작된 AST를 기반으로 Byte code를 작성한다.

위의 과정을 거치기 때문에 Lombok 어노테이션이 코드를 대체할 수 있었다. 만약 Enable annotation processing 옵션을 켜지 않는다면 Lombok은 AST를 조작할 기회를 가질 수 없을 것이고, 최종적으로는 Lombok이 정상적으로 동작할 수 없었을 것이다.

Lombok 동작 원리
Lombok 동작 원리 이해하기
애노테이션 프로세서(Annotation processor)

회고

느낀 점

과제를 수행하기 위해서는 처음보는 여러 기능들을 활용해야 했다. 그리고 이 기능들을 활용하기 위해서는 이들을 공부해야 했다. 그 덕분에 처음 보는 다양한 기능들을 공부할 수 있었고, 나아가 기존에 확신없이 사용하던 기능들에 대해 더 자세히 알아볼 수 있었다. 2주차 주제인 JDBC에 걸맞게 스프링 부트에서 JDBC를 사용하는 방법에 대해 자세히 공부하는 시간을 가질 수 있어서, 스프링 부트와 JDBC에 대해 기반을 단단히 가진 것 같아서 좋았다. 다음 주차에는 이번 스터디의 마지막인 Spring Core를 공부한다. 스프링의 핵심인 만큼 다음 주도 열심히 공부해야겠다.

1개의 댓글

comment-user-thumbnail
2023년 11월 29일

몰랐던 내용들 많이 알고 갑니다!
사소한 것들에 대해서도 궁금해하는 모습이 보기 좋네요!!

답글 달기