내일배움캠프 Spring 49일차(화) TIL

Skadi·2024년 3월 5일
0

스프링 JPA

1. 개인과제

  • Service 클래스를 인터페이스와 구현체 형태로 변경
  • Service 클래스 내 메소드마다의 책임을 1개에 가깝게 줄이기
  • 수정 전 Service
@Service
@RequiredArgsConstructor
public class CommentService {

    private final CommentRepository commentRepository;
    private final ScheduleRepository scheduleRepository;


    public CommentResponseDto createComment(Long scheduleId, CommentRequestDto commentRequestDto,
        UserDetailsImpl userDetails) {
        Schedule schedule = scheduleRepository.findById(scheduleId).orElseThrow(
            NotFoundScheduleException::new
        );
        Comment comment = new Comment(commentRequestDto.getComment(), schedule, userDetails.getUser());
        commentRepository.save(comment);

        return new CommentResponseDto(comment);
    }

    @Transactional
    public CommentResponseDto updateComment(Long commentId, CommentRequestDto commentRequestDto,
        UserDetailsImpl userDetails) {
        Comment comment = commentRepository.findById(commentId).orElseThrow(
            NotFoundCommentException::new
        );

        if (!(comment.getUser().getUserId() == userDetails.getUser().getUserId())) {
            throw new UnauthorizedOperationException();
        }

        comment.update(commentRequestDto.getComment());

        return new CommentResponseDto(comment);
    }

    public void deleteComment(Long commentId, UserDetailsImpl userDetails) {

        Comment comment = commentRepository.findById(commentId).orElseThrow(
            NotFoundCommentException::new
        );

        if (!(comment.getUser().getUserId() == userDetails.getUser().getUserId())) {
            throw new UnauthorizedOperationException();
        }

        commentRepository.delete(comment);

    }
}
  • 수정 후 Service
public interface CommentService {

    /**
     * 댓글 생성
     *
     * @param scheduleId        댓글을 생성할 게시글 ID
     * @param commentRequestDto 댓글 생성 내용
     * @param userDetails       댓글 생성자 구분
     * @return 게시글 생성 결과
     */
    CommentResponseDto createComment(Long scheduleId, CommentRequestDto commentRequestDto,
        UserDetailsImpl userDetails);

    /**
     * 댓글 수성
     *
     * @param commentId         수정할 댓글 ID
     * @param commentRequestDto 수정할 댓글 내용
     * @param userDetails       댓글 수정자 구분
     * @return 게시글 수정 결과
     */
    CommentResponseDto updateComment(Long commentId, CommentRequestDto commentRequestDto,
        UserDetailsImpl userDetails);

    /**
     * 댓글 삭제
     *
     * @param commentId   삭제할 댓글 ID
     * @param userDetails 댓글 삭제자 구분
     */
    void deleteComment(Long commentId, UserDetailsImpl userDetails);
}


---

@Service
@RequiredArgsConstructor
public class CommentServiceImpl implements CommentService {

    private final CommentRepository commentRepository;
    private final ScheduleRepository scheduleRepository;


    @Override
    public CommentResponseDto createComment(
        Long scheduleId,
        CommentRequestDto commentRequestDto,
        UserDetailsImpl userDetails
    ) {
        Schedule schedule = findScheduleById(scheduleId);
        Comment comment = new Comment(commentRequestDto.getComment(), schedule,
            userDetails.getUser());
        commentRepository.save(comment);

        return new CommentResponseDto(comment);
    }

    @Override
    @Transactional
    public CommentResponseDto updateComment(
        Long commentId,
        CommentRequestDto commentRequestDto,
        UserDetailsImpl userDetails
    ) {

        Comment comment = findCommentById(commentId, userDetails);

        comment.update(commentRequestDto.getComment());

        return new CommentResponseDto(comment);
    }

    @Override
    public void deleteComment(Long commentId, UserDetailsImpl userDetails) {

        Comment comment = findCommentById(commentId, userDetails);

        commentRepository.delete(comment);

    }

    private Schedule findScheduleById(Long scheduleId) {
        return scheduleRepository.findById(scheduleId)
            .orElseThrow(NotFoundScheduleException::new);
    }

    private Comment findCommentById(Long commentId, UserDetailsImpl userDetails) {
        Comment comment = commentRepository.findById(commentId).orElseThrow(
            NotFoundCommentException::new
        );

        if ((Objects.equals(comment.getUser().getUserId(), userDetails.getUser().getUserId()))) {
            return comment;
        }
        throw new UnauthorizedOperationException();
    }
}
  • 장점

    • 유연성과 확장성 향상: 인터페이스를 통해 서비스의 계약을 정의하면, 다양한 구현체를 쉽게 교체하거나 추가할 수 있습니다. 이는 유연성과 확장성을 크게 향상시킵니다.
    • 테스트 용이성: 인터페이스를 사용하면, 테스트 시에 실제 구현체 대신 모의 객체(Mock Object)나 스텁(Stub)을 사용할 수 있어, 단위 테스트가 용이해집니다.
    • 의존성 역전 원칙(DIP) 지원: 인터페이스를 사용하면, 고수준 모듈이 저수준 모듈에 의존하지 않고, 둘 다 추상화에 의존하게 됩니다. 이는 소프트웨어 설계의 견고함을 높이는 데 도움이 됩니다.
    • AOP(Aspect-Oriented Programming) 적용 용이: 스프링 프레임워크에서 AOP를 적용할 때, 인터페이스 기반 프록시를 자동으로 생성할 수 있어, 다양한 공통 관심 사항(로깅, 트랜잭션 관리 등)을 효율적으로 관리할 수 있습니다.
  • 단점

    • 개발 복잡성 증가: 각 서비스마다 인터페이스와 구현체를 작성해야 하므로, 초기 개발 과정에서 복잡성이 증가할 수 있습니다.
    • 오버엔지니어링 위험: 프로젝트의 규모나 복잡도에 비해 과도하게 인터페이스를 사용하면, 오버엔지니어링의 위험이 있습니다. 이는 유지 보수성을 저해하고, 개발 효율성을 떨어뜨릴 수 있습니다.
    • 학습 곡선: 인터페이스와 구현체를 분리하는 방식은 객체 지향 설계 원칙과 스프링 프레임워크에 대한 이해를 요구합니다. 따라서, 초보 개발자에게는 학습 곡선이 높을 수 있습니다.
    • 런타임 성능 고려 사항: 인터페이스를 통한 동적 프록시 생성 등은 미미하지만 추가적인 런타임 오버헤드를 발생시킬 수 있습니다. 대부분의 애플리케이션에서는 이러한 성능 차이가 눈에 띄지 않지만, 고성능이 필수적인 애플리케이션에서는 고려해야 할 수 있습니다.

2. ORM의 성장과정

2-1 릴레이션(관계형 데이터베이스)를 객체(도메인 모델)로 매핑 하려는 이유?

  • 객체 지향 프로그래밍의 장점을 활용할 수 있다.
  • 이를 통해, 비즈니스 로직 구현 및 테스트 구현이 편리함
  • 각종 디자인 패턴 사용하여 성능 개선 가능
  • 코드 재사용

2-2 ORM이 해결해야 하는 문제점과 해결책

  • 상속의 문제

    • 객체 : 객체간에 멤버변수나 상속관계를 맺을 수 있다.
    • RDB : 테이블들은 상속관계가 없고 모두 독립적으로 존재한다.
    • 해결방법 : 매핑정보에 상속정보를 넣어준다. (@OneToMany, @ManyToOne)
  • 관계 문제

    • 객체 : 참조를 통해 관계를 가지며 방향을 가진다. (다대다 관계도 있음)
    • RDB : 외래키(FK)를 설정하여 Join 으로 조회시에만 참조가 가능하다. (즉, 다대다는 매핑 테이블 필요)
    • 해결방법 : 매핑정보에 방향정보를 넣어준다. (@JoinColumn, @MappedBy)
  • 탐색 문제

    • 객체 : 참조를 통해 다른 객체로 순차적 탐색이 가능하며 콜렉션도 순회한다.
    • RDB : 탐색시 참조하는 만큼 추가 쿼리나, Join 이 발생하여 비효율적이다.
    • 해결방법 : 매핑/조회 정보로 참조탐색 시점을 관리한다.(@FetchType, fetchJoin())
  • 밀도 문제
    • 객체 : 멤버 객체크기가 매우 클 수 있다.
    • RDB : 기본 데이터 타입만 존재한다.
    • 해결방법 : 크기가 큰 멤버 객체는 테이블을 분리하여 상속으로 처리한다. (@embedded)
  • 식별성 문제
    • 객체 : 객체의 hashCode 또는 정의한 equals() 메소드를 통해 식별
    • RDB : PK 로만 식별
    • 해결방법 : PK 를 객체 Id로 설정하고 EntityManager는 해당 값으로 객체를 식별하여 관리 한다.(@Id,@GeneratedValue )

2-3 ORM이 얻은 최적화 방법

  • 1차 캐시
    • 영속성 컨텍스트 내부에는 엔티티를 보관하는 저장소가 있는데 이를 1차 캐시라고 한다.
    • 일반적으로 트랜잭션을 시작하고 종료할 때까지만 1차 캐시가 유효하다.
    • 1차 캐시는 한 트랜잭션 계속해서 원본 객체를 넘겨준다.
  • 2차 캐시
    • 애플리케이션 범위의 캐시로, 공유 캐시라고도 하며, 애플리케이션을 종료할 때 까지 캐시가 유지된다.
    • 2차 캐시는 캐시 한 객체 원본을 넘겨주지 않고 복사본을 만들어서 넘겨준다.
    • 복사본을 주는 이유는 여러 트랜잭션에서 동일한 원본객체를 수정하는일이 없도록 하기 위해서이다.
    • 2차캐시 적용방법
      1. Entity에 @Cacheable 적용 후 설정 추가
      2. sharedCache.mode 설정

2-4 영속성 컨텍스트(1차 캐시)를 활용한 쓰기지연

  • 영속성 이란?
    • 데이터를 생성한 프로그램이 종료되어도 사라지지 않는 데이터의 특성을 말한다.
    • 영속성을 갖지 않으면 데이터는 메모리에서만 존재하게 되고 프로그램이 종료되면 해당 데이터는 모두 사라지게 된다.
    • 그래서 우리는 데이터를 파일이나 DB에 영구 저장함으로써 데이터에 영속성을 부여한다.
  • 쓰기 지연이 발생하는 시점
    • flush() 동작이 발생하기 전까지 최적화한다.
    • flush() 동작으로 전송된 쿼리는 더이상 쿼리 최적화는 되지 않고, 이후 commit()으로 반영만 가능하다.
  • 쓰기 지연 효과
    • 여러개의 객체를 생성할 경우 모아서 한번에 쿼리를 전송한다.
    • 영속성 상태의 객체가 생성 및 수정이 여러번 일어나더라도 해당 트랜잭션 종료시 쿼리는 1번만 전송될 수 있다.
    • 영속성 상태에서 객체가 생성되었다 삭제되었다면 실제 DB에는 아무 동작이 전송되지 않을 수 있다.
    • 즉, 여러가지 동작이 많이 발생하더라도 쿼리는 트랜잭션당 최적화 되어 최소쿼리만 날라가게된다.
  • 키 생성전략이 generationType.IDENTITY 로 설정 되어있는 경우 생성쿼리는 쓰기지연이 발생하지 못한다.
    • 단일 쿼리로 수행함으로써 외부 트랜잭션에 의한 중복키 생성을 방지하여 단일키를 보장한다.

0개의 댓글