본문 바로가기

개발

디자인 패턴을 적용해보자 - 팩토리(1). (pizza 예제 아님)

0. 시작하는 이유


헤드 퍼스트의 디자인 패턴 책을 보았지만 실제 코드에서 적용하긴 쉽지 않다. 책의 예시가 실무에서 접할 상황이 아니라 객체나라에서 펼쳐지는 피자와 피자매장의 이야기 이기에 내가 진행중인 플젝에서 적용해보려고 한다. 

내 프로젝트는 spring boot + jpa 환경이라서 예제 코드도 같은 환경으로 깃허브에 해당 코드를 올려두었다. 상황별로 태그를 달아 두었으니 clone 후 태그를 체크아웃 하면서 설명해보겠다.

https://github.com/SongHae8640/designPatternApply

 

GitHub - SongHae8640/designPatternApply

Contribute to SongHae8640/designPatternApply development by creating an account on GitHub.

github.com

 

커맨드에서 git fetch origin 이후에 git tag를 입력했을때 아래와 같이 나오면 준비 끝이다.

 

 

 

 

 

 

 

1. 상황


상황은 다음과 같다. 게시글 도메인과 음료 도메인이 있고 이 두 도메인에 댓글을 달 수 있다.

댓글을 조회할때는

1. 해당 타입의 도메인이 있는지 여부를 확인한다.

2. 해당 타입의 도메인의 댓글수 를 +1 해준다.

3. 댓글의 타입과 해당 타입의 id를 를 댓글 내용과 함께 저장한다.

 

 

git checkout tags/factory_base 명령어를 입력해서 코드로 함께 봐보자.

 

엔티티 코드이다.

@Entity
@Table(name = "drink")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
@ToString
public class Drink {

    @Id
    @Column(name = "id")
    @GeneratedValue(strategy = GenerationType.SEQUENCE
            , generator = "DRINK_SEQ_GEN")
    @SequenceGenerator(name = "DRINK_SEQ_GEN",
            sequenceName = "DRINK_SEQ",
            allocationSize = 1)
    private Long id;

    @Column(name = "name", nullable = false)
    private String name;


    @Column(name = "description")
    private String description;

    @Column(name = "comment_count", nullable = false)
    private Long commentCount;

    @PrePersist
    public void prePersist(){
        this.commentCount = 0L;
    }

    public void addCommentCount() {
        this.commentCount++;
    }
}




@Entity
@Table(name = "board")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
@ToString
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE
            , generator = "DISCOVER_SEQ_GEN")
    @SequenceGenerator(name = "DISCOVER_SEQ_GEN",
            sequenceName = "DISCOVER_SEQ",
            allocationSize = 1)
    private Long id;

    @Column(name = "title", nullable = false)
    private String title;

    @Column(name = "content", nullable = false)
    private String content;

    @Column(name = "comment_count", nullable = false)
    private Long commentCount;

    @PrePersist
    public void prePersist(){
        this.commentCount = 0L;
    }

    public void addCommentCount() {
        this.commentCount++;
    }
}


@Entity
@Table(name = "comment")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@ToString
@Builder
public class Comment{

    @Id
    @Column(name = "id")
    @GeneratedValue(strategy = GenerationType.SEQUENCE
            , generator = "COMMENT_SEQ_GEN")
    @SequenceGenerator(name = "COMMENT_SEQ_GEN",
            sequenceName = "COMMENT_SEQ",
            allocationSize = 1)
    private Long id;

    @Column(name = "comment_type")
    @Enumerated(EnumType.STRING)
    private CommentType commentType;

    @Column(name = "types_id", nullable = false)
    private Long typesId;

    @Column(name = "writer_name", nullable = false)
    private String writerName;

    @Column(name = "password", nullable = false)
    private String password;

    @Column(name = "content", nullable = false)
    private String content;
}

 

 

Service 코드이다. 조회 및 저장할때 비슷하게 반복되는 코드가 보일것이다.

@Service
@RequiredArgsConstructor
public class CommentService {

    private final CommentRepository commentRepository;
    private final DrinkRepository drinkRepository;
    private final BoardRepository boardRepository;


    public List<Comment> getCommentList(CommentType commentType, Long typesId) {
        if(commentType == CommentType.DRINK) {
            drinkRepository.findById(typesId)
                    .orElseThrow(() -> new NoSuchElementException("존재하지 않는 음료입니다."));
        } else if(commentType == CommentType.BOARD) {
            boardRepository.findById(typesId)
                    .orElseThrow(() -> new NoSuchElementException("존재하지 않는 게시글입니다."));
        }
        return commentRepository.findAllByCommentTypeAndTypesId(commentType, typesId);
    }

    public Comment saveComment(Comment comment) {
        if(comment.getCommentType() == CommentType.DRINK) {
            drinkRepository.findById(comment.getTypesId())
                    .orElseThrow(() -> new NoSuchElementException("존재하지 않는 음료입니다."))
                    .addCommentCount();
        } else if(comment.getCommentType() == CommentType.BOARD) {
            boardRepository.findById(comment.getTypesId())
                    .orElseThrow(() -> new NoSuchElementException("존재하지 않는 게시글입니다."))
                    .addCommentCount();
        }
        return commentRepository.save(comment);
    }
}

 

 

 

 

 

2. 테스트


위 코드에서는 엔티티와 Serivce 만 있지만 Controller 로 외부 요청을 받는 API를 구성하였다. (깃헙 코드 참조) 

이 API가 정상적으로 작동하는지를 확인 하는 테스트 코드를 작성하고 각 변화에서 응답이 같은지를 확인하려고 한다.

@SpringBootTest
@AutoConfigureMockMvc
class CommentControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Autowired
    CommentRepository commentRepository;
    @Autowired
    BoardRepository boardRepository;
    @Autowired
    DrinkRepository drinkRepository;

    Long savedBoardId;
    Long savedDrinkId;


    @BeforeEach
    void setUp() {
        Board board = boardRepository.save(Board.builder()
                .title("제목")
                .content("내용")
                .build());
        savedBoardId = board.getId();

        Drink drink = drinkRepository.save(Drink.builder()
                .name("코카콜라")
                .description("코카콜라는 맛있다.")
                .build());
        savedDrinkId = drink.getId();

        commentRepository.save(Comment.builder()
                .content("댓글")
                .writerName("tester")
                .password("1234")
                .commentType(CommentType.BOARD)
                .typesId(savedBoardId)
                .build());

        commentRepository.save(Comment.builder()
                .content("댓글")
                .writerName("tester")
                .password("1234")
                .commentType(CommentType.DRINK)
                .typesId(savedDrinkId)
                .build());
    }

    @Test
    void 음료_댓글_기본_조회() throws Exception {
        // Given
        String commentType = "drink";
        long typesId = 1L;
        String url = "/api/comments?commentType=" + commentType + "&typesId=" + typesId;

        // When
        ResultActions actions = mockMvc.perform(get(url)

        );


        // Then
        System.out.println("------------------");
        actions.andExpect(status().isOk());
        System.out.println(actions.andReturn().getResponse().getContentAsString());

    }

    @Test
    void 음료_댓글_기본_저장() throws Exception {
        // Given
        String url = "/api/comments";
        String commentType = "drink";
        Long typesId = 1L;
        CommentDto commentDto = new  CommentDto();
        commentDto.setCommentType(commentType);
        commentDto.setTypesId(typesId);
        commentDto.setWriterName("테스터");
        commentDto.setContent("테스트 코멘트");
        commentDto.setPassword("1234");



        // When
        ResultActions actions = mockMvc.perform(post(url)
                .content(new ObjectMapper().writeValueAsString(commentDto))
                .contentType(MediaType.APPLICATION_JSON)
        );


        // Then
        System.out.println("------------------");
        actions.andExpect(status().isOk())
                .andExpect(jsonPath("$.content").value(commentDto.getContent()))
                .andExpect(jsonPath("$.writerName").value(commentDto.getWriterName()))
                .andExpect(result -> {
                    System.out.println("status :: " + result.getResponse().getStatus());
                    System.out.println("body :: " + result.getResponse().getContentAsString());
                });
        assertEquals(1 , drinkRepository.findById(typesId).get().getCommentCount());
    }
}

테스트 결과는 아래 사진 처럼 정상이다.

 

 

 

3. 문제점


게시글과 음료 외에 댓글 타입이 추가가 된다면 Service 코드에 if 절이 추가 해야하기 때문에  OCP를 위배하게 된다.

또한, findById 를 통해 객체의 생성하는 부분과 댓글 수를 더하는 로직이 한 클래스 내에 있어 SRP 를 위배하고 있다.

때문에 조건으로 분기하는 부분을 팩토리를 이용하여 이 코드를 개선해보려고 한다.

 

2번째 포스팅에서는 simple factory 를 이용하여 리펙토링 해보고, 3번째 포스팅에서는 factory method pattern 을 이용하여 리펙토링 해보겠다.