오늘 할일

  • 스프링 학습
    • exception handler
    • dispatcher servlet
    • SpringBootTest, WebMvcTest
  • 테코톡 진행

@SpringBootTest vs @WebMvcTest

@SpringBootTest의 기본 설정인 WebEnvironment=Mock 설정과 @WebMvcTest를 사용하는 테스트는 둘 다 실제 서버를 구동하지 않고 웹 환경울 구성하여 테스트할 수 있다. 하지만 서로 다른 목적과 범위를 가지고 있다.

  1. @SpringBootTest
  • 통합 테스트의 목적으로, 애플리케이션의 전체적인 동작을 확인합니다.
  • 전체 애플리케이션 컨텍스트를 로드하여 통합 테스트를 수행한다.
  • 웹 계층 뿐만 아니라, 데이터베이스, 서비스 계층, 다른 설정 등 모든 빈과 설정이 로드된다.
  1. @WebMvcTest
  • 웹 계층의 단위 테스트의 목적으로, 컨트롤러(Controller)와 관련된 로직을 확인한다.
  • 웹 관련 빈만 로드하고, 다른 빈이나 설정은 로드하지 않습니다. 예를 들어, 서비스 계층이나 데이터베이스 계층의 빈은 로드되지 않는다.

요약하면, 두 방식 모두 웹 환경의 구성을 테스트할 수 있지만, 로드하는 빈과 설정의 범위가 다르다. @SpringBootTest(WebEnvironment=Mock)은 전체 애플리케이션 컨텍스트를 로드하여 통합 테스트를 수행하고, @WebMvcTest는 웹 계층에 특화된 단위 테스트를 수행한다. 웹 계층만 테스트하는 경우라면, @WebMvcTest를 사용하는 것이 더 적합하고 효율적이다.

'회고 > 우아한테크코스' 카테고리의 다른 글

2023.04.25 일일 회고  (0) 2023.04.26
2023.04.24 일일 회고  (0) 2023.04.25
2023.04.19 일일 회고  (0) 2023.04.20
2023.04.18 일일 회고  (0) 2023.04.19
2023.04.17 일일 회고  (0) 2023.04.18

오늘 할일

  • InnoDB의 MVCC
  • 웹 자동차 경주 미션 2단계 리팩터링 및 제출
  • 테코톡 인수인계 받기
  • 이동욱님 특강 듣기
  • 스프링 학습
    • Controller
    • ExceptionHandler

MVCC(Multi Version Concurency Control)

mysql의 기본 엔진인 InnoDB는 MVCC를 지원한다. MVCC란 무엇일까?

먼저 트랜잭션의 isolation level(격리 수준) 을 이해 해야 한다. isolation level은 여러 트랜잭션이 동시에 처리될 때 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있게 허용할지를 결정하는 것이다. 총 4가지의 수준으로 나뉘어져 있다.

  • READ UNCOMMITTED - 트랜잭션이 커밋되지 않아도 변경된 데이터를 볼 수 있다.
  • READ COMMITTED - 커밋되지 않은 트랜잭션의 데이터는 볼 수 없다.
  • REPEATABLE READ - 동일한 트랜잭션 내에서 동일한 쿼리를 여러 번 수행해도 결과는 같다.
  • SERIALIZABLE - 트랜잭션이 순차적으로 진행된다.

이러한 isolation level에 따라서 동시에 발생하는 트랜잭션에 대해서 동시성 제어를 하는 것이 MVCC이다.

InnoDB 스토리지 엔진은 REPEATABLE READ 격리 수준을 사용하고 있다. InnoDB에서 트랜잭션에서 데이터를 변경했을 때 트랜잭션의 커밋 유무에 따라 결과가 달라진다.

  • 롤백했을 때 - 언두 영역의 백업된 데이터를 다시 데이터 파일로 복구한다.
  • 커밋했을 때 - 현재 상태가 그대로 유지된다.

커밋을 하지 않아도 원본 데이터가 언두 영역에 저장되고, 현재 상태가 바뀌는 것이다. 따라서 READ UNCOMMITTED는 언두 영역을 조회하지 않고 원본 테이블을 조회하기 때문에 커밋되지 않았어도 변경된 데이터를 볼 수 있다. 이를 Dirty read라고 한다.
READ COMMITTED는 트랜잭션이 커밋되지 않으면 언두 영역을 조회하여 원본 데이터를 조회한다.
REPEATABLE READ는 해당 시점의 데이터베이스 상태를 나타내는 일관된 스냅샷이 생성된다. 이 스냅샷은 트랜잭션 도중 발생하는 SELECT 쿼리에 사용된다. 현재 상태가 스냅샷과 일치하지 않으면 언두 로그를 확인해서 데이터의 이전 상태를 추적한다.

건강하게 나아지기

이동욱님께서 루터회관에 오셔서 특강을 해주셨다. 워낙 유명하신 분이라 시작부터 높은 위치에서 시작했을 것이라고 생각했으나 틀린 생각이었다. 그저 꾸준히 열심히 하고 계속해서 학습을 잘하고 있나 검증을 하셨다. 요즘 학습의 효율이 크게 나오지 않는 것 같다. 시간만 보낸다고 안주하고 있지 않고 학습을 제대로 하고 있는지 검증을 해야 할 것 같다.

이동욱님은 업무를 하면서 진행했던 과정, 각종 해결했던 이슈들에서 적용한 기술에 대해서 블로그로 전부 남기셨다. 나도 기술 블로그를 작성하고 싶었는데 이를 단순히 기술만 옮겨 적는 것이 아니라 내가 직접 적용하면서 느꼈던 것들을 바탕으로 기술 블로그를 작성해보고자 한다.

'회고 > 우아한테크코스' 카테고리의 다른 글

2023.04.24 일일 회고  (0) 2023.04.25
2023.04.20 일일 회고  (0) 2023.04.21
2023.04.18 일일 회고  (0) 2023.04.19
2023.04.17 일일 회고  (0) 2023.04.18
2023.04.13 일일 회고  (0) 2023.04.14

오늘 할일

  • 강의
  • 학습테스트
  • 웹 자동차 경주 미션 2단계 완성
  • 스프링 학습
    • Controller
    • ExceptionHandler
    • @Component, @Repository, @Service, @Controller 차이

@Component, @Repository, @Service, @Controller 차이

@Repository, @Service, @Controller 는 모두 @Component 기반으로 빈을 등록하는 역할이다.

@Repository

Exception translation : 예외 전환이다. Jdbc를 사용하면 DB 작업 중에는 SQLException이 발생하고 다른 DB를 사용하면 SQLException이 아니라 다른 Exception이 발생한다. @Respository는 이러한 예외들을 DataAccessException으로 전환을 해준다.

@Service

별다른 추가 기능 없이 서비스 클래스라는 것을 명시.

@Controller

@RequestMapping 기반의 handler 메서드를 이용해서 사용한다. classpath scanning 을 통해 클래스들이 자동으로 찾아지도록 함.

추가 기능을 만드는(Repository의 예외 전환이나, @Transactional) 작업은 스프링 AOP가 프록시 객체를 만들어 데코레이터패턴으로 구현한다. 프록시 객체를 만들 때 인터페이스가 정의되어 있다면 JDK dynamic proxy로, 인터페이스가 없다면 CGLIB가 대상 클래스를 extends하여 구현한다.

  • JDK dynamic proxy : 리플렉션으로 사용하는 메서드를 가져와서 사용(속도가 느림)

  • CGLIB : 해당 메서드를 상속하서 사용. 따라서 클래스와 메서드에 final이 붙어있다면 예외가 발생한다.

스프링의 DI 방식 3가지

필드 주입, setter 주입, 생성자 주입이 있습니다.
주로 생성자 주입을 사용했습니다. 필드 주입이나 setter 주입을 사용하게 되면 객체의 생성 이후에 의존성이 주입 되기 때문에 불변을 보장할 수 없습니다. 생성자에서 주입하게 되면 불변을 보장할 수 있고 객체의 생성 시점에 필요한 의존성이 모두 주입 되기 때문에 필요한 의존성을 쉽게 파악할 수 있습니다.

스프링에서 빈으로 관리할 대상

상태가 없는 객체를 빈으로 관리해야 한다고 생각합니다.
상태가 있다는 것은 상태에 따라 행동이 달라진다는 것을 뜻합니다. 빈은 싱글턴으로 객체를 하나만 생성해서 공유하기 때문에 공유된 상태를 가지고 행동을 하게되면 문제가 발생할 수 있습니다.

unchecked exception을 사용하는 이유

예외에는 checked exception과 unchecked exception이 있다.

checked exception은 예외가 발생할 수 있으니 예외 처리를 강제하는 방식이고, unchecked exception은 그것을 자유로 맡기는 방식이다.
checked exception의 경우 상위 메서드로 계속해서 예외를 전달하게 된다. 이러한 경우 OCP를 위반하게 된다. 변경이 생겼을 때 중간에서 예외를 전달하는 메서드들도 다 수정해야 하기 때문이다.
예외의 원인에는 복구 가능한 오류와 복구가 불가능한 오류가 있다.

복구 가능한 오류(네트워크 문제 등)는 checked exception을 발생시켜 사용자가 예외를 복구하도록 한다. 복구 불가능한 오류는 대부분의 경우 사전 조건이 잘못 됐음을 의미한다. 따라서 예외를 발생시켜 문제가 있음을 프로그래머가 알아야 한다. 즉, 빠른 실패로 예외가 발생한 곳을 알아차릴 수 있어야 한다. 이는 uncheked exception으로 처리하여 예외를 전달, 회피, 복구를 하지 않도록 한다.

'회고 > 우아한테크코스' 카테고리의 다른 글

2023.04.20 일일 회고  (0) 2023.04.21
2023.04.19 일일 회고  (0) 2023.04.20
2023.04.17 일일 회고  (0) 2023.04.18
2023.04.13 일일 회고  (0) 2023.04.14
2023.04.12 일일 회고  (0) 2023.04.13

오늘 할일

  • 웹 자동차 경주 미션 피드백 반영
  • 근로 회의
  • 스프링 학습
    • Transactional
    • Controller
    • ExceptionHandler
  • 아코와 학습 내용 공유

웹 자동차 경주 미션

  • 최상위 도메인 객체 RacingGame 추가.
  • Repository를 객체의 컬렉션처럼 사용하도록 수정.
public ResultResponseDto play(RacingGameRequestDto racingGameRequestDto) {  
    RacingGame racingGame = initGame(racingGameRequestDto);  
    racingGame.moveCars(numberGenerator);  
    RacingGame save = racingGames.save(racingGame);  

    return new ResultResponseDto(save.getWinners(), mapToPlayerDtos(save.getCars()));  
}

racingGames가 RacingGame의 Repository이다. racingGame 객체를 컬렉션처럼 단순히 save하면 repository 구현체 내부에서 알아서 DB에 테이블을 나눠서 저장한다. 이렇게 만들면 서비스 레이어에서는 비즈니스 로직을 사용하듯 POJO를 지켜서 데이터를 저장할 수 있다.

  • ConsolerController와 WebController가 같은 서비스를 사용하도록 중복 제거.

@Transactional

Transaction이란?

하나의 작업 묶음을 의미하고, 이 작업 묶음에 대해 ACID를 보장하도록 한다.

ACID

  • 원자성 : 이 작업 묶음이 실패하면 모두 실패하거나, 성공하면 모두 성공해야 한다.
  • 일관성 : 트랜잭션의 이전, 이후 데이터베이스의 상태는 이전과 같이 유효해야 한다.
  • 고립성 : 모든 트랜잭션은 다른 트랜잭션으로부터 고립되어야 한다.
  • 영속성 : 하나의 트랜잭션이 성공했다면 이후에 어떤 오류가 발생해도 해당 기록은 영구적이어야 한다.

@Transactional 을 메서드에 붙이면 메서드를 Transaction으로 만들어주고, 클래스에 붙이면 모든 메서드를 Transaction으로 만들어준다.

왜 서비스의 메서드를 Transaction으로 관리할까? 서비스의 메서드도 하나의 작업 묶음이기 때문이다. 서비스의 메서드에서 일어나는 모든 영속적인 작업을 Transaction으로 만들어 ACID를 보장하도록 하는 것이다.

동작 과정

Spring AOP가 타겟 클래스의 인터페이스를 통해 프록시 객체를 만든다.
Transaction으로 만들기 위해 부가 기능을 추가하기 위해 프록시 객체를 만들어서 실행하는 것이다.
인터페이스가 없다면 CGLIB (Code Generation Library)를 사용하여 생성한다. 다만, 스프링에서는 인터페이스를 구현하는 클래스에 @Transactional을 사용하는 것을 권장한다.


미션을 진행하면서 느낀 스프링의 장점

웹 관련 기능을 추가하면서 스프링을 적용해주셨는데, 어떤 장점을 느끼셨나요?! 마코는 스프링이 제공해주는 장점이 무엇이 있을지 생각해보신적이 있으실까요? 한번 같이 간단하게 이야기해보면 좋을 것 같습니다!

스프링을 사용하면서 느낀 가장 큰 장점은 관심사의 분리입니다. 기존에는 의존성들을 직접 주입해주고, 트랜잭션의 ACID를 보장하려면 직접 코드로 작성해야 하지만 이러한 작업들을 스프링에서 담당해주고, 비즈니스 로직에 집중할 수 있다는 것이 가장 큰 장점이라고 생각합니다. 아직은 스프링의 기능 중 일부만 사용해봤지만 다른 기능들도 같은 목표를 달성하기 위해 사용할 것 이라고 생각합니다. 또한, POJO를 지킬 수 있다는 것이 장점인 것 같습니다. 다른 프레임워크는 아직 사용해보지 않았고 글로만 POJO를 지킨다 라고 배우기는 했지만 실제로 비즈니스 레이어까지는 계속해서 객체를 사용할 수 있다는 것을 느꼈습니다.

'회고 > 우아한테크코스' 카테고리의 다른 글

2023.04.19 일일 회고  (0) 2023.04.20
2023.04.18 일일 회고  (0) 2023.04.19
2023.04.13 일일 회고  (0) 2023.04.14
2023.04.12 일일 회고  (0) 2023.04.13
2023.04.11 일일 회고  (0) 2023.04.12

침범된 Layered architecher

웹 자동차 경주 미션을 진행하면서 웹 컨트롤러를 작성했다.

웹 컨트롤러는 자동차의 이름을 입력받고, 게임 진행을 위한 서비스를 초기화하고, 게임을 진행한다.

@PostMapping("/plays")  
@ResponseBody  
public ResponseEntity<ResultResponseDto> play(@RequestBody RacingGameRequestDto racingGameRequestDto) {  
    List<String> names = Arrays.stream(racingGameRequestDto.getNames().split(","))  
            .map(String::trim)  
            .collect(Collectors.toList());  // 이름 입력

    RacingCarService racingCarService = new RacingCarService(  
            Cars.of(names),  
            new RandomNumberGenerator(),  
            new RacingCarRepository(new RacingCarGameDao(jdbcTemplate))  
    );  // 서비스 초기화
    racingCarService.play(racingGameRequestDto.getCount()); // 게임 진행
    ...
}

컨트롤러에서 도메인 객체인 Cars 를 생성해서 서비스를 생성하고 있다. 보통은 서비스를 빈으로 등록하여 컨테이너에서 관리하도록 하는데, 지금의 방식은 비즈니스 로직에 집중하기 어렵다. 그럼 어떤 구조로 만들어야 할까?

WebController는 Layered architecher 상에서 프레젠테이션 레이어에 속한다. 요청에 따라 서비스를 이용하여 적절한 응답을 반환해준다. 어떤 도메인 객체를 사용하는지 WebController 가 알게된다면 서비스레이어가 어떤 구조로 작동하는지 안다는 것이다.

이렇게 레이어를 침범한 구조이기 때문에 원하는 방식으로 Cars직접 생성해서 Service에 직접 주입해줄 수 밖에 없는 구조가 되어버렸다. 따라서 IoC 컨테이너가 의존성을 자동으로 주입해줄 수 없게 되어버렸다.

각 Layer의 역할이 무엇인지 영역을 침범하지 않았는지 고민하면서 다시 리팩토링을 진행해서 IoC컨테이너가 의존성을 알아서 컨트롤하도록 리팩토링 하는 것이 목표다.

DAO와 Repository

DAO나 Repository나 현업에서는 비슷하게 사용한다는 얘기를 들었다. (굳이 구분하지 않는다고 한다.)

dao는 db를 통해 데이터를 가져오고자 하는 목적이다.
db로부터 가져오겠다는 것을 숨기지 않기 때문에 영속적을 보장하겠다는 것이다.

Repository는 도메인을 가져오고자 하는 목적이다.
dao의 추상적인 개념이다 라고 하기도 하는데 잘 와닿지가 않는다.
Repository는 도메인만 가져오면 되기 때문에 Repository.get() 과 같이 도메인을 가져와 주면 되고, 내부적으로 dao를 사용해서 도메인을 반환해주든, 식별자 맵을 통해서 반환해주든, 다른 datasource를 사용해서 반환해주든 상관 없는 것이다. 이러한 특성때문에 영속성을 보장하지 않는다고 한다.
그저 도메인을 add,get과 같이 저장하고 꺼내기 때문에 객체의 컬렉션이라고 부르기도 한다.

식별자 맵

DB에 접근하는 비용은 비싸기 때문에 한 번 꺼낸 데이터는 식별자 맵에 저장해놓고 같은 요청이 들어왔을 때 이를 반환해줄 수 있다.

'회고 > 우아한테크코스' 카테고리의 다른 글

2023.04.18 일일 회고  (0) 2023.04.19
2023.04.17 일일 회고  (0) 2023.04.18
2023.04.12 일일 회고  (0) 2023.04.13
2023.04.11 일일 회고  (0) 2023.04.12
[레벨 1] 체스 미션 회고  (0) 2023.04.10

오늘 한일 :

자동차 경주 미션을 웹 요청/응답

IoC 컨테이너

객체가 생성자 인자, 팩토리 메서드의 인자, 생성된 후 객체 인스턴스의 속성에 설정하는 방식으로만 의존성(즉, 다른 객체들과 함께 작업하는 객체)을 정의하고, 컨테이너가 빈을 생성할 때 해당 의존성을 주입한다.
이 과정은 빈 자체가 클래스의 생성자를 직접 사용하거나 서비스 로케이터 패턴과 같은 메커니즘에 의해 인스턴트화하거나 위치를 제어하는 것과 반대되는 개념이다.

org.springframework.beans and org.springframework.context 는 스프링 프레임워크의 IoC 컨테이너의 기본 패키지다.

BeanFactory interface는 모든 유형의 객체를 관리할 수 있는 고급 구성 메커니즘을 제공한다.
ApplicationContextBeanFactory의 하위 인터페이스이다. 다음과 같은 특성을 추가로 가진다.

  • Easier integration with Spring’s AOP features
  • Message resource handling (for use in internationalization)
  • Event publication
  • Application-layer specific contexts such as the WebApplicationContext for use in web applications.

Container Overview

org.springframework.context.ApplicationContext 인터페이스는 Spring IoC 컨테이너를 표현하고 빈을 인스턴스화, 설정, 조합하는 책임을 가진다.

DI

  • 생성자 주입
  • 필드 주입
  • setter 주입

아코랑 페어 마지막날인데 너무 슬프다....ㅠㅠ

아코가 먼저 취업하면 맥북을 사준다고 했다. 열심히 응원하자.

'회고 > 우아한테크코스' 카테고리의 다른 글

2023.04.17 일일 회고  (0) 2023.04.18
2023.04.13 일일 회고  (0) 2023.04.14
2023.04.11 일일 회고  (0) 2023.04.12
[레벨 1] 체스 미션 회고  (0) 2023.04.10
[레벨 1] 블랙잭 미션 회고  (0) 2023.03.30

레벨 2를 시작했다.

새로운 것들을 많이 접해서 나름 신난다.

근로도 시작하고 인생 처음 스프링을 접했다.

방대한 스프링에 대한 두려움이 있었으나 학습 테스트로 학습을 하니까 결과를 바로 확인하고 왜 동작하는지 이유도 알아보면서 학습하는 즐거움이 있었다.

SPRING MVC, SPRING JDBC에 대해서 학습 테스트를 진행했는데, 먼저 SPRING MVC에 대한 내용만 정리를 해본다.

RestAssured

RESTful 웹 서비스의 테스트를 간편하게 할 수 있도록 도와준다.
given, when, then 패턴으로 테스트를 명확하게 파악할 수 있다.

  • given : content-type, body 등 설정 get은 리소스를 요청할 때 Accept를 사용하여 서버로부터 받고 싶은 미디어 타입을 지정하고, post는 body 미디어 타입을 Content-type으로 지정해 준다.
  • when : post 또는 get 설정
  • then : status, cookie, body, header, content-type 등을 검증

Request Mapping

컨트롤러 메서드에 request를 매핑하기 위해 @RequestMapping 을 사용할 수 있다.
URL, HTTP 메서드, request parameters, headers, media types를 매치하기 위한 여러 속성들이 있다.
공유된 매핑을 표현하기 위해 클래스 레벨이나, 특정 엔드포인트 매핑으로 줄이기 위해 메서드 레벨에서 사용할 수 있다.

  • @GetMapping
  • @PostMapping
  • @PutMapping
  • @DeleteMapping
  • @PatchMapping

@ResponseBody

HttpMessageConverter 가 리턴 값을 직렬화 해서 body에 담아 전송함.

ResponseEntity

HTTP 응답의 모든 정보를 담고 있는객체.

public class ResponseEntity<T> extends HttpEntity<T> {  

   private final Object status;
   ...
}

HttpEntity는 다음과 같다.

public class HttpEntity<T> {  

   /**  
    * The empty {@code HttpEntity}, with no body or headers.  
    */   public static final HttpEntity<?> EMPTY = new HttpEntity<>();  


   private final HttpHeaders headers;  

   @Nullable  
   private final T body;
}

body는 제네릭 타입이기 때문에 밖에서 타입을 지정해 줄 수 있다.
생성자를 사용할 수 있지만 빌더를 권장(숫자로된 상태 코드보다는 메서드로 하는 것이 실수 적음)

return ResponseEntity.ok().body(new User(id, "이름", "email")); 와 같이 생성할 수 있다.

@PathVariable

@GetMapping("/users/{id}") 과 같은 어노테이션에서 캡쳐된 URI 즉, {id} 변수에 접근할 수 있도록 해줌.

메서드의 파라미터에서 @PathVariable Long id 와 같이 변수명을 맞추거나 @PathVariable("id") Long name 과 같이 매핑해서 사용할 수 있다.

Pattern

  • "/resources/ima?e.png" - ?에는 어느 문자가 와도 매치됨
  • "/resources/*.png" - .png 앞에 어느 문자가 와도(없어도) 매치됨
  • "/resources/**" - 다중 경로까지 매치
  • "/projects/{project}/versions" - 경로를 매치하고 변수를 캡쳐
  • "/projects/{project:[a-z]+}/versions" - 경로와 변수의 정규식에 해당하면 매치하고 변수 캡쳐

@RequestMapping

  • name : 요청 매핑의 이름을 지정
  • value / path : 요청 URL 지정
  • method : 기본값 GET, HTTP 요청 메서드(POST, PUT, DELETE)등
  • params : 매개변수 지정
  • headers : 요청 헤더 지정
  • consumes
  • produces

consumes vs produces

  • consumes : request의 매핑 범위를 Content-type 기반으로 줄임. post에 대한 media type 매핑
  • produces : request의 매핑 범위를 Accept 기반으로 줄임. get에 대한 media type 매핑

@RequestParam vs @RequestBody

  • RequestParam : 서블릿 request 파라미터를 메서드의 인자로 바인딩하기 위해 사용
  • RequestBody : HttpMessageConverter 에 의해 body를 역직렬화해서 객체로 만듬

Thymeleaf

컨트롤러가 전달하는 데이터를 이용해 동적으로 화면 만들어주는 뷰 템플릿 엔진.
순수 HTML을 유지하기 때문에 내추럴 템플릿으로도 불림.

'회고 > 우아한테크코스' 카테고리의 다른 글

2023.04.13 일일 회고  (0) 2023.04.14
2023.04.12 일일 회고  (0) 2023.04.13
[레벨 1] 체스 미션 회고  (0) 2023.04.10
[레벨 1] 블랙잭 미션 회고  (0) 2023.03.30
[레벨 1] 레벨 로그  (0) 2023.03.28

느낀점

시작은 수월했으나 좌절을 여러 번 느끼게 했던 미션이었다.

주드와 호흡이 잘 맞아서 구현을 막힘 없이 진행할 수 있었고 2번째로 pr을 빠르게 제출하였다.

실제 도메인과 비슷하게 구현하려고 칸을 만들고 이를 이중 리스트로 만들어 보드를 구현하였다.

칸에는 말이 존재하고, 말은 위치를 모른다.

정보전문가 패턴에 따라서 책임을 부여하였고 책임 분리는 실제로 잘 되었다.

그러나 먼저 실제 도메인을 흉내 내고 책임을 부여하다 보니 객체들의 협력이 느슨하거나 부자연스러운 객체들의 협력이 완성되었다.

이러한 부자연스러운 객체들의 협력은 코드를 이해하기 어렵게 만들었고, 캡슐화가 깨지는 부분도 생겼다.

결국 모든 기능을 구현해 놓고 객체의 협력을 다시 설계하여 리팩토링을 진행했다.

대공사였기 때문에 하기 싫었지만 테스트를 배제하고 리팩터링 하여 생각보다 빠르게 변경할 수 있었다.

리팩토링을 완성하고 기도를 하면서 애플리케이션을 돌렸는데 잘 돌아가서 한 숨 놓을 수 있었다.

이 방법은 고민이 필요할 것 같다. 이번에는 잘 돌아갔으나 여기저기서 터졌다면 매우 힘들었을 것이다....

레벨 1이 끝났음에도 객체를 설계하는 능력은 아직 많이 부족한 것 같다.

앞으로도 객체 지향 공부를 놓지 말자.


배운 점

배치 쿼리

쿼리를 반복적으로 수행해야 한다면 어떻게 해결할 수 있을까?

다음 코드를 보자.

@Override  
public void saveGame(ChessGame chessGame) {  

    final Board board = chessGame.board();  

    for (File file : File.values()) {  
        for (Rank rank : Rank.values()) {  

            final var query = "INSERT INTO chessGame VALUES(?, ?, ?, ?, ?)";  

            Position position = new Position(file, rank);  
            Square square = board.getSquare(position);  
            Piece piece = square.getPiece();  
            Color color = piece.getColor();  
            Kind kind = piece.getKind();  
            Class<?> pieceClass = piece.getClass();  

            try (final var connection = Loader.getConnection();  
                    final var preparedStatement = connection.prepareStatement(query)) {  
                preparedStatement.setString(1, file.name());  
                preparedStatement.setString(2, rank.name());  
                preparedStatement.setString(3, color.name());  
                preparedStatement.setString(4, kind.name());  
                preparedStatement.setString(5, pieceClass.getName());

                preparedStatement.executeUpdate();  
            } catch (final SQLException e) {  
                throw new RuntimeException(e);  
            }  
        }  
    }  
}

위 코드는 체스 보드의 File과 Rank의 모든 Piece를 저장하는 메서드이다.
따라서 8 x 8 = 64번의 insert 쿼리를 실행하게 된다.
쿼리 실행은 네트워크를 타고 서버까지 갔다 와야 하기 때문에 시간이 매우 오래 걸린다.
이를 64번 수행하면 성능이 매우 안 좋을 것이다.
이를 Batch를 통해 해결할 수 있다.

@Override  
public void saveGame(ChessGame chessGame) {  

    final var query = "INSERT INTO chessGame VALUES(?, ?, ?, ?, ?)";  

    try (final var connection = Loader.getConnection();  
         final var preparedStatement = connection.prepareStatement(query)) {  

        final Board board = chessGame.board();  

        for (File file : File.values()) {  
            for (Rank rank : Rank.values()) {  

                Position position = new Position(file, rank);  
                Square square = board.getSquare(position);  
                Piece piece = square.getPiece();  
                Color color = piece.getColor();  
                Kind kind = piece.getKind();  
                Class<?> pieceClass = piece.getClass();  

                preparedStatement.setString(1, file.name());  
                preparedStatement.setString(2, rank.name());  
                preparedStatement.setString(3, color.name());  
                preparedStatement.setString(4, kind.name());  
                preparedStatement.setString(5, pieceClass.getName());  

                preparedStatement.addBatch();  
            }  
        }  

        preparedStatement.executeBatch();  
    } catch (final SQLException e) {  
        throw new RuntimeException(e);  
    }  
}

preparedStatement.addBatch() 를 통해 한 번의 쿼리를 배치에 저장하고, 모든 배치를 다 저장하면 preparedStatement.executeBatch() 로 배치를 실행한다. 이렇게 하면 네트워크를 한 번만 이동할 수 있다.

취약한 기반 클래스

이번 미션에서 자식클래스가 구현된 부모 클래스의 메서드를 오버라이딩 하여 발생했다.

부모클래스의 변경이 자식클래스에 영향을 미치면 안 된다.

Optional 활용

Entity는 테이블의 열과 직접적으로 매핑되는 클래스이다.
다음 클래스를 보자.

public class ChessGameEntity {  

    private final Long gameId;  
    private final String userName;  
    private final String gameTurn;

    private static ChessGameEntity of(String userName, String gameTurn) {
        this.gameId = null;
        ...
    }
}

userName과 gameTurn에 해당하는 데이터를 테이블에 삽입할 때 쿼리문은 다음과 같을 것이다.

INSERT INTO chess_game('user_name', 'game_turn') VALUES(?, ?)

이때, gameId는 필요하지 않다.
따라서 gameId를 null로 초기화해 주거나, 생성자에서 기본 값으로 초기화해주거나, final 키워드를 제거하여 생성 시 초기화를 하지 않아도 되도록 하는 방법 중에 선택해야 한다.

기본값(-1과 같은)을 사용하는 것과 null을 사용하는 것, final 키워드를 제거하기

  • 기본값 : 처리하는 쪽에서 해당 값에 대해서 알아야 처리할 수 있어 복잡해진다. 잘못된 값임에도 DB에는 정상적인 값으로 인식되어 잘못된 데이터가 저장되고, 문제가 바로 발견되지 않아 나중에 더 큰 문제로 돌아올 가능성이 있음.
  • null : null을 반환하면 사용자가 null에 대한 검사를 반드시 해야 하고, 이는 프로그램을 불안정하게 만들 수 있다.
  • final 제거 : setter와 같은 재할당을 허용하여 불변을 보장할 수 없다.

Optional을 반환하도록 한다면 null 검사를 강제할 수 있다.

public Optional<Long> getGameId() {  
    return Optional.ofNullable(gameId);  
}

Optional을 필드에도 사용해 보고, 매개변수로 사용해 본 적이 있다. IDE에서 경고를 띄워주기도 하고 가독성도 떨어진다고 생각하여 Optional의 필요성을 느끼지 못했다. 이번 경험을 통해 null일 수 있는 값을 반환할 때 Optional로 래핑 해서 반환하여 사용한다는 것을 배웠다.

추가로 다음과 같은 피드백도 받았다.

어플리케이션의 관점에서 null을 사용하지 않는 개념은 좋은 방향이라고 생각해요. 하지만 외부 시스템과의 연계를 생각하게 된다면, null이라는 개념이 필요한 케이스도 있습니다.
예를들면, jpa를 사용하기 위해 null을 사용하지 않는 코틀린에서 null을 허용하는 케이스도 있어요

null을 무조건 사용하고 싶지 않았으나 무조건 안 쓰기보다는 상황에 따라 필요한 경우도 있기 때문에 상황을 잘 판단하도록 하자.

var의 장점

movablePathSet<Position> 에서 MovablePositions 일급 컬렉션으로 래핑 하는 수정이 있었습니다.
이때 var를 사용했기 때문에 변경할 필요가 없었습니다.

즉, 타입에 종속되지 않아서 리팩토링을 수행하더라도 로직이 변경되지 않는 한 변경이 적다는 장점이 있었습니다.

타입의 이름이 길더라도 이를 줄여주는 장점도 있는 것 같습니다.

DIP 위반과 Service의 재사용

체스게임의 컨트롤러 로직을 Command 패턴을 통해 구현했다.

그중에서 각 커맨드에서 필요한 서비스를 직접 생성해서 사용했다.

public class StartCommand extends ActionCommand {
    @Override

public void execute(final String input) {
    final var loadService = new LoadService();
    ...
}

이렇게 되면 다른 Command에서 LoadService가 필요하면 새로운 LoadService를 인스턴스화해서 사용할 것이다. 즉, Service 를 재사용하지 않고 필요할 때마다 새로 생성한다는 것이 문제다.
또한, LoadService의 구체적인 구현에 의존하고 있기 때문에 DIP를 위반하고 있다. 이는 변경에도 취약하고 테스트도 어렵게 만든다. LoadService 를 추상화하여 Loadable 같은 인터페이스를 주입받도록 한다면 결합도를 낮춰 Service가 바뀌더라도 코드 변경은 없거나 매우 적을 것이다. 또한, Loadable 을 구현하는 fake를 만들어 테스트하거나 스텁을 통해 테스트를 하기 훨씬 수월할 것이다.

Gradle의 종속성 선언

  • api : 종속된 라이브러리까지 가져온다. 종속된 라이브러리를 모두 빌드하기 때문에 크기가 커지고 시간이 오래 걸릴 수 있다. 사용하려면 plugins의 id를 java-library로 선언해야 한다.
  • implementation : 선언한 라이브러리만 빌드한다.
  • compile : 의존성을 컴파일클래스패스와 런타임클래스패스에 선언하기 때문에 충돌 등의 문제로 gradle 5.0부터 deprecated 됨
  • compileOnly : 컴파일타임에만 사용하는 의존성을 선언 ex) lombok
  • runtimeOnly : 런타임에만 사용하는 의존성을 선언 ex) JDBC

라이브러리를 만드는 것이 아니라면 api 보다는 implementation을 사용한다

DTO Assembler

처음에는 DAO에서 데이터를 꺼내고, 도메인 객체로 조립하여 바로 컨트롤러로 전달하도록 했다. 이는 DAO의 책임이 너무 크다고 생각해 처음으로 Entitiy, Repository, Service의 개념을 도입하여 책임을 분리하였다.
DB에서 Board 정보를 꺼내올 때 구조는 다음과 같다.

DAO가 DB의 정보를 테이블과 직접적으로 매핑되는 Entity에 담아서 Repository로 전달한다.
Repository는 매핑 클래스의 도움을 받아 Entitiy를 도메인 객체인 Board 로 매핑하고, 다시 DTO 매핑 클래스인 DTOAssembler 를 통해 DTO로 매핑한다. 이 DTO를 Service로 전달하고, 다른 컨트롤러나 이를 사용하는 Service에서 DTO를 다시 도메인 객체인 Board 로 조립하여 사용한다. 이렇게 하면 중간에 Board의 상태를 변경시키지 못하고 데이터를 안전하게 전송할 수 있다.

자연키와 대체키

board 테이블은 체스판에서 위치와 위치에 해당하는 piece의 정보를 담고 있다.

하나의 체스판에서 같은 위치두 개 이상의 piece가 존재할 수 없기 때문에 다음과 같이 복합키를 사용했다.

PRIMARY KEY (game_id,piece_file,piece_rank)

이에 대해서 다음과 같은 피드백을 받았다.

복합키를 기본키로 쓰는 것과, auto increment와 같은 것을 기본키로 쓸 때의 장단점을 알아볼까요?

복합키를 기본키로 쓰게 됐을 때 단점은 다음과 같습니다. (자연키)

  • 변경에 취약하다
    의미 있는 값으로 기본키를 설정했기 때문에 나중에 요구사항이 변경되면 테이블 설계를 다시 해야 한다.
  • 자동으로 인덱스를 생성해주지 않아 전체 테이블을 검색해야 하므로 성능이 떨어진다. 인덱스를 작성해 주더라도 열의 수가 많아지면 인덱스를 관리하는데 비용이 커진다.

auto increment와 같은 것 (대체키)

  • 요구사항이 변경되어도 변경될 일이 적다.

그렇다면 대체키를 사용하고 필요하다면 유니크 인덱스를 걸어주는 게 나을 것 같습니다.
하지만 서비스 환경에 따라 성능이 중요하다면 굳이 DB 쪽에서 중복을 처리해 주기보다 애플리케이션을 믿고 처리를 해주지 않는 것도 괜찮다는 생각이 드네요.

'회고 > 우아한테크코스' 카테고리의 다른 글

2023.04.12 일일 회고  (0) 2023.04.13
2023.04.11 일일 회고  (0) 2023.04.12
[레벨 1] 블랙잭 미션 회고  (0) 2023.03.30
[레벨 1] 레벨 로그  (0) 2023.03.28
2023.03.27 일일 회고  (0) 2023.03.28

+ Recent posts