상세 컨텐츠

본문 제목

[TIL#23] 가을에 시작한 Spring <IoC/DI>

내배캠/Chapter3

by DK9 2023. 11. 28. 20:42

본문

+ 방금 알게 된 사실 : 서비스는 서비스를 참조할 수 있지만 단방향으로만 참조해야 한다. 양쪽에서 동일하게 참조할 시 순환참조가 되기 때문에 하면 안 된다.


 또한 두 가지 관점이 있다. 이는 개인의 선택에 따라 결정된다. 아래의 코드는 2번에 해당하는 코드이다.

  1. 서비스는 해당 도메인의 레파지토리 '하나만' 참조하는 구조, 다른 도메인의 저장된 정보가 필요하다면 다른 도메인의 서비스를 참조해서 해야 한다는 관점.
  2. 서비스는 서비스는 서비스를 참조할 수 '없다'. 그러니 다른 도메인의 저장된 정보가 필요하다면 다른 도메인의 레파지토리를 참조해야 한다는 관점.
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class LikeService {

    private final PostRepository postRepository;
    private final CommentRepository commentRepository;
    private final LikeRepository likeRepository;

    @Transactional
    public LikeResponseDto pressLike(User user, Long postId, Long commentId) {
        Post post = checkPost(postId);
        Comment comment = checkComment(commentId);

        Likes likes = likeRepository.findByUserAndPostAndComment(user, post, comment)
            .orElseGet(() -> saveCommentLike(user, post, comment));

        Boolean updated = likes.updateLike();
        comment.updateLikeCnt(updated);

        return LikeResponseDto.of(likes.getIsLiked());
    }

    @Transactional
    public Likes saveCommentLike(User user, Post post, Comment comment) {

        Likes likes = Likes.builder()
            .user(user)
            .post(post)
            .comment(comment)
            .isLiked(DEFAULT_LIKE)
            .build();

        return likeRepository.save(likes);
    }

    private Comment checkComment(Long commentId) {
        return commentRepository.findById(commentId).orElseThrow(
            () -> new CommentExistsException(CommentErrorCode.NOT_EXISTS_COMMENT));
    }

    private Post checkPost(Long postId) {
        return postRepository.findById(postId).orElseThrow(
            () -> new CommentExistsException(CommentErrorCode.NOT_EXISTS_POST));
    }

}

 

작성 완료한 이 코드를 이용하여 알고 있는 개념들을 정리해 보겠다.

1. IoC 와 DI

 간단하게 이해한 내용을 기술하겠다.

 1) IoC 제어의 역전

  • 객체 관리의 주체가 사람(프로그래머)에서 시스템(프로그램, 컴퓨터)으로 바뀌는 것
  • 파인 다이닝에서 셰프가 메뉴의 구상에 전념하고 재료관리 혹은 조리상태를 하위 구성원에게 맡기듯
    프로그래머는 비즈니스 로직 구성에 전념하고 객체관리는 시스템에 맡겨버리는 것이다.
  • 객체들 간에 결합(=의존성)을 약화시켜 유연한 프로그램을 만들 수 있다.
  • 따라서 유지보수성과 재사용성을 높이고 프로그래머의 작업 난이도를 낮춰준다. 근데 뒤질 것 같은데?

2) DI 의존성 주입

  • IoC 를 달성하기 위한 여러 방법 중 하나. IoC 를 달성하기 위한 디자인 패턴 중 하나이다. IoC가 목적지라면 DI는 가는 방법이다. 그렇기에 IoC의 개념이 녹아있는 IoC의 하위 개념이다.
  • 프로그래머가 A클래스를 만들고 시스템에 등록하면(Spring의 경우@Component 등을 이용해 Bean으로 등록) B클래스에서 A가 필요한 경우 B클래스에서 인스턴스화하지 않고 시스템에 요청하여 받아오는 것을 말한다.
  • 현재 가장 권장되는 방법은 생성자를 이용한 방법이다. 생성자가 단 하나일 경우 @(어노테이션)를 별도로 달지 않아도 의존성 주입이 된다.
@Service
public class UserService {

    private UserRepository userRepository;
    private MemberService memberService;

    @Autowired
    public UserService(UserRepository userRepository, MemberService memberService) {
        this.userRepository = userRepository;
        this.memberService = memberService;
    }
    
}

출처: https://mangkyu.tistory.com/125 [MangKyu's Diary:티스토리]

 

하지만 위의 코드에서는 생성자가 보이지 않을 것이다. 그 이유는 @RequiredArgsConstructor 때문이다.

 

3) 생성자 관련 Lombok

 Lombok은 Java 라이브러리로 반복 메서드 작성 코드를 줄여주는 코드 다이어트 라이브러리다.(ex. Getter, Setter 등)

 

  • @RequiredArgsConstructor
    : final 변수처럼 필수적인 정보를 자동으로 세팅하여 생성자를 만들어준다. 그래서 PostRepository, CommentRepository, LikeRepository 의 생성자가 생략된 것이다.
  • @NoArgsConstructor
    : 기본 생성자를 생성해 준다. 이
    경우 초기값 세팅이 필요한 final 변수가 있을 경우 컴파일 에러가 발생함으로 주의한다.
  • @AllArgsConstructor
    : 
    클래스 내부에 선언된 모든 필드를 매개 변수로 가지는 생성자를 생성한다. 단, 모든 필드를 전부 매개 변수로 가지는 생성자이기에 매개 변수를 선별해서 받아야 할 경우 null 값이 나오기에 에러가 발생한다.

위의 코드를 보면 초기값 세팅이 필요한 final 변수가 있다. 그렇기에 @RequiredArgsConstructor 를 사용했다.

다음으로 @Service 이것은 무엇일까?

 

 4) Service Component

 역시 컴포넌트로 DI의 한 부분이다. Spring MVC에서 Model과 관련되어 있다. 즉 @Controller가 클라이언트로 받아온 요청에 따른 비즈니스 로직을 수행하는 Component이다. @Controller >> @Service >> @Repository 로 이어지는 Spring MVC 흐름에서 Service를 담당하고 있다는 것을 프로그램에게 알려주기 위한 것이라고 이해하고 있다.

  •  최상단의 코드를 보면 @Service 클래스 내부에는 Repository를 final 변수로 받고 있다. 즉 클라이언트로 요청을 받는 역할을 수행하는 @Controller 클래스 안에는 Service를 final 변수로 받고 있을 것이다. final 변수가 있으니 @RequiredArgsConstructor 를 사용했음을 알 수 있다.

 그럼 마지막으로 남은 @Transactional 은 무엇일까?

 

 5) Transactional

힘들게 데이터베이스에 저장한 데이터를 망치면 얼마나 억울한가. Transactional 은 데이터의 정합성을 지키기 위한 어노테이션이다. 제대로 된 설명을 하기 위해서는 꽤나 깊게 들어가야 하고 필자는 그 정도의 실력은 안되기 때문에 현재는 대략적으로 이런 느낌이다 정도로 이해하고 있는 수준이다.

 Transacrtonal 은

  • 데이터를 수정, 삭제할 때 사용해야 한다.
  • 막고라 신청이다. 끝날 때까지 누구도 간섭하지 못한다.
  • 정상적인 막고라가 아니라면 막고라 이전 상태로 되돌린다.
  • 우선순위가 있고 가장 가까운 Transactional 의 영향을 받는다.

정도로 이해하고 있다. 그리고 Transactional 을 readonly 로 한 것은 어떤 이유가 있는지 모르겠지만 팀원이 이 방식이 좀 더 데이터 안정성(?)을 높일 수 있다고 했다. 막연하게 생가하기에는 Transactional 을 통해 읽기 전용으로 만들고 필요한 부분에서 Transaction을 사용하므로 데이터가 변질될 위험이 적다고 생각되는데 적지 않은 것과 어떤 차이가 있는지 아직은 모르겠다.

 

 6) Builder

 마지막으로 빌더패턴이다. 최상단의 코드에서는 빌더를 사용해 Likes likes 를 만드는 부분만 있고 빌더 생성자의 모습은 보여주지 않고 있다.

 빌더패턴을 사용하는 이유는 단순하다.

  1. 편의성
  2. 가독성
  3. 객체의 불변성

을 위해서 사용한다.

 

 @Builder 는 클래스 레벨과 생성자 레벨에서 사용할 수 있다. 자세히는 모르지만 클래스 레벨에서 사용하는 것은 좀 까다롭고 다양한 제약사항이 존재한다고 알고 있다.

 가장 큰 차이는

  1. 클래스 레벨의 Builder 는 모든 필드를 입력받아야 하지만
  2. 생성자 레벨의 Builder 는 생성자의 파라미터 필드에 대해서만 빌더 메서드를 생성한다는 점이다.

 그렇기에 매개 변수를 설정할 수 있고 직접 사용해 본 생성자 레벨의 @Builder 를 정리하겠다.

@Builder
private Likes(Long id, User user, Post post, Comment comment, Boolean isLiked) {
    this.id = id;
    this.user = user;
    this.post = post;
    this.comment = comment;
    this.isLiked = isLiked;
}

 

빌더 생성자이다. Likes 라는 객체를 만들 때 필요한 매개 변수들을 일목요연하게 볼 수 있다. 그리고 사용할 때는 최상단의 코드 처럼 .builder( ) 로 빌더를 연 다음 매개 변수들을 .~~.@@.##.$$ 이런 식으로 연달아서 작성한 후 .build( )로 빌더를 닫아주면 된다.

 

최상단의 코드 중에서 Repository 에서 필요한 Likes를 가져오는 쿼리문도 코드 속에 있지만 아직 정리할 만큼 소화하지 못한 것 같아서 나중에 정리하겠다는 다짐을 하며 이만 글을 줄이겠다.

관련글 더보기