1단계 완료, 2단계 완료
1단계 학습 주제: 웹 요청과 응답을 처리하기 위한 Spring MVC의 기능
2단계 학습 주제: 데이터베이스 접근을 위한 Spring JDBC
is()
"1단계 - 예약 조회"의 테스트 코드를 추가하는 과정에서 위와 같이 is()
메서드가 인식되지 않는 현상이 발생했다. IntelliJ가 제시하는 import 명단에도 적절해보이는 라이브러리가 표시되지 않았고, 웹서핑을 해보아도 RestAssured
라이브러리의 .body()
메서드에 함께 쓰인다는 내용 뿐 is()
메서드의 출처가 명시된 페이지는 찾을 수 없었다.
그렇게 포기하고 스터디 커뮤니티에 질문글을 작성하던 중, 마지막으로 GPT에게 질문이라도 해보자는 생각이 들었다. GPT는 org.hamcrest.Matchers
라이브러리에 있는 is()
메서드를 import하면 해결할 수 있다는 답변을 내놓았고, 실제로 그렇게 import하니 문제가 해결되었다.
아직 테스트 코드에 관한 지식과 웹서핑 실력이 많이 부족하다는 것을 느낄 수 있는 시간이었다...
AtomicLong
은 Long
자료형을 가지는 Wrapper
클래스이다. Long
이라는 래퍼 클래스가 기존에 존재하는데 왜 AtomicLong
이라는 새로운 클래스가 필요한 것일까? 이는 멀티 스레드 환경과 관련이 있다.
멀티 스레드 환경에서는 하나의 자원에 대해 여러 스레드에서 동시에 접근하는 일이 일어나는데, 이 때 동시성 환경에서의 값의 불일치 문제가 발생할 수 있다.
이를 막기 위해 스레드 동기화라는 작업을 진행하기도 하는데, 이는 멀티스레드 환경에서 여러 스레드가 하나의 공유자원에 동시에 접근하지 못하도록 막는 것(lock) 을 말한다. 이러한 공유 데이터, 즉 동기화가 필요한 부분을 임계 영역(Critical Section) 이라고 하며, 여기에 synchronized
키워드를 붙여 스레드 동기화를 수행하게 할 수 있다.
하지만 이렇게 되면 하나의 스레드가 공유 자원에 접근 중인 경우 다른 스레드는 해당 자원에 접근할 수 없게 막혀(lock) 자신의 순서를 기다리게 되는데, 이는 곧 성능적인 문제로 이어진다. 이런 문제를 해결하기 위해 논블로킹(non-blocking
) 알고리즘 중 하나인 CAS(CompareAndSwap
)을 적용할 수 있다.
자바 - Synchronized 스레드 동기화 개념 및 사용예제
CompareAndSwap
) 알고리즘CAS 알고리즘은 "현재 스레드에 저장된 값과 메인 메모리에 저장된 값을 비교"하여 일치할 경우 새로운 값으로 교체하고, 일치하지 않는다면 실패 후 재시도한다. 이 과정을 거치면 가시성 문제가 해결된다.
가시성 문제
멀티 스레드 환경에서 각 CPU는 자신의 캐시 메모리에서 값을 참조한다. 하지만 메인 메모리 값과 이 캐시 메모리에서의 값이 다른 경우가 있는데, 이를 가시성 문제라고 한다.
[운영체제] Atomic연산, CAS(CompareAndSwap)에 대하여, ABA문제
AtomicLong
은 무엇인지?AtomicLong
은 멀티 스레드 환경에서 값의 불일치 문제 없이 안전하게(thread-safe) 연산을 수행할 수 있도록 지원하는 Long
에 대한 Wrapper
클래스이다. synchronized
와 달리 CAS 알고리즘을 적용하여 연산 과정을 최적화하였다.
추가로 JAVA에서는 AtomicLong
뿐만 아니라 AtomicInteger
, AtomicBoolean
, AtomicReference
를 제공한다고 한다. 이러한 Atomic
클래스에 저장된 값을 수정하기 위해서는 별도로 제공되는 메서드를 사용해야만 한다.
Long 과 AtomicLong은 어떤 차이가 있을까?
@ExceptionHandler
는 Controller 단에서 발생하는 예외를 잡아와서 메서드로 쉽게 처리할 수 있도록 도와주는 어노테이션이다. @ExceptionHandler
를 사용할 때 처리할 예외를 함께 명시하지 않으면 모든 예외를 잡기 때문에 처리할 예외를 꼭 명시해주는 것이 좋다.
@ControllerAdvice
를 붙인 advice 클래스를 만들고 그 안에서 @ExceptionHandler
메서드를 작성한다. 이는 Application 전역(Global)에서 일어나는 예외를 잡을 때 사용한다.@ExceptionHandler
메서드를 작성한다.기본적으로는 @ControllerAdvice
클래스에서 예외를 잡아 처리한다. 하지만 예외가 발생한 Controller 내에 그에 대한 @ExceptionHandler
메서드가 존재한다면 그 곳에서 예외 처리를 진행한다.
[스프링부트] @ExceptionHandler를 통한 예외처리
Spring boot 공부 3 - 예외 처리 ControllerAdvice와 @ExceptionHandler
스프링 부트에서는 Spring JDBC의 초기 데이터(DataSource
) 세팅 기능을 제공한다. 애플리케이션 로딩 시 src/main/resources
에 위치한 schema.sql
과 data.sql
파일을 기반으로 초기 데이터를 설정한다.
DDL(Data Definition Language, 데이터 정의어) 를 작성하는 파일
1. 데이터베이스 스키마(구조와 구성요소)를 정의한다.
2. 테이블, 뷰, 인덱스 등을 정의한다.
DML(Data Manipulation Language, 데이터 조작어) 를 작성하는 파일
1. 데이터베이스에 삽입할 초기 데이터를 정의한다.
2. 데이터베이스를 초기화하고 초기 상태를 설정한다.
schema.sql
이 먼저 실행되고 data.sql
이 이후에 실행된다.
스프링에서는 DataSourceInitializer
빈을 사용하여 schema.sql
과 data.sql
파일을 실행한다. DataSourceInitializer
는 데이터베이스의 초기화를 담당하는 인터페이스로, 빈으로 등록되어 있으면 자동으로 실행된다. 해당 파일에서 data-source
와 schema.sql
파일의 위치를 지정할 수 있다.
[Spring/DB] Spring에서 schema.sql과 data.sql 파일은 어떻게 다르고 어떤 순서로 실행될까?
schema.sql data.sql - velog
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)
BeanPropertyRowMapper
와 BeanPropertySqlParameterSource
BeanPropertyRowMapper
는 Bean 객체를 기반으로 RowMapper
를 생성한다.
BeanPropertySqlParameterSource
는 Bean 객체를 기반으로 SqlParameterSource
를 생성한다.
두 클래스는 각각 RowMapper
와 SqlParameterSource
를 알아서 생성해주기 때문에 JdbcTemplate
이나 SimpleJdbcInsert
를 사용할 때 VO 클래스 정보를 일일이 매핑시키지 않아도 되서 편리하다. 하지만 알아서 매핑해주는 만큼 해당 클래스의 필드 목록이 반드시 일치해야 한다.
query()
와 execute()
와 update()
의 차이JdbcTemplate
에는 쿼리를 날리는 방법으로 query()
와 execute()
, 그리고 update()
메서드를 지원한다. 셋은 메서드가 구분된 만큼 용도가 나뉘어 사용된다.
query()
query()
는 보통 SELECT
와 같이 데이터베이스에서 데이터를 검색하여 결과 집합을 반환하는 쿼리에 사용된다. 결과 집합은 객체 목록으로 반환되기 때문에 RowMapper
또는 ResultSetExtractor
를 통해 결과 집합의 행에 매핑해주어야 한다.
execute()
와 update()
execute()
는 INSERT
, UPDATE
, DELETE
를 포함한 모든 SQL문을 실행할 때 주로 사용한다. 즉, DDL을 작성할 때 사용한다고 볼 수 있다. PreparedStatementCreator
와 StatementCallback
를 사용할 수 있도록 제공해주고, 이를 수정하여 개발자가 원하는 결과를 만들 수 있다.
update()
는 INSERT
, UPDATE
, DELETE
와 같은 데이터 수정에 관한 쿼리를 날릴 때 주로 사용된다. 즉, DML을 작성할 때 사용한다고 볼 수 있다. 반환값으로는 SQL 쿼리 실행으로 인해 영향을 받은 행 수를 반환한다.
위에서 update()
는 DML 외의 쿼리 수행이 불가능한 것처럼 이야기했지만 실제로는 가능하다. 그래서 execute()
와 update()
는 동작 자체는 거의 동일하고 실제로 혼용하여 사용하기도 한다. 하지만 세부적으로 들어가면 차이가 있기에 그 차이를 알고 용도에 맞게 사용하는 것이 좋아 보인다.
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를 공부한다. 스프링의 핵심인 만큼 다음 주도 열심히 공부해야겠다.
몰랐던 내용들 많이 알고 갑니다!
사소한 것들에 대해서도 궁금해하는 모습이 보기 좋네요!!