초록 스터디 3주차

송선권·2023년 12월 7일
2

초록 스터디

목록 보기
3/5
post-thumbnail

3주차 스터디 진행 상황

3단계 완료

3단계 학습 주제: 스프링의 객체 관리 기능을 위한 Spring Core
(+기본적인 스프링 애플리케이션의 구조인 Layered Architecture)

스터디 진행 과정

의존성 주입 방법

2주차 PR 피드백을 받는 과정에서 의존성을 필드 주입으로 받는 이유에 대한 질문이 들어왔다.

필드 주입(Field Injection)

@Autowired  
private ReservationDao reservationDao;
  • 다른 방법에 비해 의존성 주입이 쉬워서 단일 책임 원칙(SRP)을 위반하기 쉬워진다.
  • DI Container에 의존해서는 안된다.
    - 스프링 외의 프레임워크로 동작시킬 경우를 감안하여 DI Container에 대한 의존성이 없어야 한다.
    - 필드 주입의 경우 다른 방법으로는 의존성 주입이 힘들다.
  • final로 선언할 수 없어서 불변성이 깨진다.
  • 순환 의존성을 잡을 수 없다.
    - A에서 B에 의존하고 B에서 A에 의존하는 경우
  • 장점: 사용하기 편하다.
  • 단점: 그 외 전부

세터 주입(Setter Injection)

@Autowired  
public void setReservationDao(ReservationDao reservationDao) {  
    this.reservationDao = reservationDao;  
}
  • 상황에 따른 의존성 주입에 유용하다.
  • Setter를 호출하여 의존성 주입을 해두지 않고 로직을 수행하면 NullPointerException이 발생한다.

생성자 주입(Constructor Injection)

@Autowired  
public ReservationService(ReservationDao reservationDao) {  
    this.reservationDao = reservationDao;  
}
  • Spring Framework Reference에서 가장 권장하는 방식이다.
  • 필수 의존성 없이는 Instance 생성을 막을 수 있다.
  • DI Container로부터 완전히 분리할 수 있다.
    - 별도의 방법으로 의존성 주입이 가능하다.
  • null을 주입하지 않는 한 NullPointerException이 발생하지 않는다.
  • final을 사용할 수 있다.
  • 순환 의존성을 잡을 수 있다.
  • 의존성 주입이 번거롭다.
    - 생성자 매개변수가 늘어날수록 위기감을 느낄 수 있다.
    - 단일 책임 원칙(SRP)을 지키기 좋다.

[Spring] DI(Dependency Injection) 세 가지 방법

결론

생성자 주입을 사용하자!

BeanProperty ...

BeanPropertyRowMapperBeanPropertySqlParameterSource를 사용하는 부분을 리팩토링하는 과정에서 새로운 것을 알게 되었다.

BeanPropertyRowMapper 공식문서, BeanPropertySqlParameterSource 공식문서

1. 필드명을 DB 필드명으로

public class ReservationVo {
    private Long reservation_id;
    ...
      
	public Long getReservation_id() {  
	    return reservation_id;  
	}  
	  
	public void setReservation_id(Long reservationId) {  
	    this.reservation_id = reservationId;  
	}
}

이들은 전달받은 클래스의 필드 이름들을 통해 자동 매핑을 진행한다. 때문에 ResultMap 용도로 사용할 VO 클래스를 작성할 때 위와 같이 쿼리에서 사용되는 필드명을 그대로 사용했다.

2. 필드명을 일반 변수 네이밍으로

public class ReservationVo {
    private Long reservationId;
    ...
	    
	public Long getReservationId() {  
	    return reservationId;  
	}  
	  
	public void setReservationId(Long reservationId) {  
	    this.reservationId = reservationId;  
	}
}

혹시 될까 싶어서 네이밍을 스네이크 케이스에서 카멜 케이스로 수정하고 실행해 보았다. 잘 실행되는 것을 보고 이게 어떻게 되는 건가 싶어서 BeanPropertyRowMapper 공식문서를 읽어보니 카멜 케이스를 지원한다고 적혀 있었다. 😮

3. 클래스를 Record로

public record ReservationVo(Long reservationId, String name, String date, Long timeId, String timeValue) {  }
===========ERROR===========
[Request processing failed: org.springframework.beans.BeanInstantiationException: Failed to instantiate [roomescape.dao.reservation.ReservationVo]: No default constructor found] with root cause

java.lang.NoSuchMethodException: roomescape.dao.reservation.ReservationVo.<init>()

"Record로 써볼 수도 있지 않을까?" 라는 생각으로 클래스를 수정해봤지만 당연하게도 에러가 발생했다. 찾아보니 기본 생성자를 만들어서 해결할 수 있다고 하여 아래와 같이 생성해보았다.

public record ReservationVo(Long reservationId, String name, String date, Long timeId, String timeValue) {  
    public ReservationVo() {  
        this(0L, "", "", 0L, "");  
    }  
}

하지만 그 결과로는 비어있는 객체가 반환될 뿐이었다. 생각해보면 당연한 일이다. BeanPropertyRowMapper는 Getter/Setter를 통해 매핑을 수행한다. 따라서 Setter가 제공되지 않는 Record 클래스를 사용하면 비어있는 객체가 반환되는 것이다.

4. DataClassRowMapper

ResultMap의 역할을 하는 VO 성격 상 Record 클래스로 정의하는 것이 제일 바람직하다고 생각한 나는 다른 방법을 찾아보았고, DataClassRowMapper를 이용하여 해결할 수 있었다.

public List<Reservation> getAllReservations() {  
    return jdbcTemplate.query("""  
		SELECT   
		    r.id as reservation_id,   
		    r.name,   
		    r.date,   
		    t.id as time_id,   
		    t.time as time_value   
		FROM reservation as r inner join time as t on r.time_id = t.id 
            """, new DataClassRowMapper<>(ReservationVo.class))  
        .stream()  
        .map(ReservationVoMapper::voToDomain)  
        .toList();  
}

DataClassRowMapper는 이름 그대로 데이터 클래스용으로 사용할 수 있는 RowMapper이다. 즉, 자바의 record 클래스나 코틀린의 data 클래스 등을 지원한다고 한다. DataClassRowMapper는 생성자의 매개변수 목록을 통해 매핑을 진행하기 때문에 가능하다.

단, 사용자 편의를 위해 제공되는 클래스인 만큼 성능이 중요하다면 별도의 RowMapper 클래스를 정의하여 사용하길 추천하고 있다.

관련 스택 오버플로우 글, DataClassRowMapper 공식문서

느낀 점

이번 주는 마지막 주차였던 만큼 특히 더 어려웠던 것 같다. 새로운 테이블을 작성하고 조회, 삽입, 삭제를 구현하는 부분은 지금까지 진행한 과정을 복습하는 느낌이었다. 특히 아직 낯선 JdbcTemplate를 이용해서 join을 해보고 이를 정해진 규격에 맞게 입출력하는 부분은 의외로 시간이 굉장히 많이 걸렸고 지쳤지만 결국 혼자 힘으로 해낼 수 있었다.
또한 리팩토링을 하면서 공식문서를 참고하는 습관을 길러야 겠다고 느꼈다. 개발자 영어는 번역기로도 잘 커버가 안되서 공식문서를 기피했었는데, 영어 그대로 읽어보니 내용이 정말 유익했다. 앞으로는 영어 페이지가 나오면 무작정 번역기부터 돌리지 말고 최대한 영어 그대로 읽어보려고 노력해야 겠다.
마지막으로 짧은 기간이었지만 초록 스터디를 진행하면서 스프링 부트에 대한 기본기가 많이 다져진 것 같다. 부트 뿐만 아니라 평소 그 원리를 자세히 모르고 의무적으로 사용하던 스프링의 여러 기능들에 대해 자세히 짚어보고 넘어갈 수 있는 좋은 시간이 되었다고 생각한다.

0개의 댓글