728x90

개인 프로젝트에 문서화가 필요할까?

 최근에 직접 만든 블로그를 운영해보고 싶어서 퇴근 후에 틈틈이 진행 중인 사이드 프로젝트가 있다. 혼자 진행하다 보니 직접 작성하고, 직접 사용할 텐데 문서화가 필요할까라는 생각이 들어 문서화는 생각하지 않고 개발 중이었다.

 

 하지만 최근에 회사에서 레거시 프로젝트 유지보수를 하면서 문서화의 필요성을 느꼈고, 가장 큰 요인으로는 스스로의 기억력에 자신도 없다. 또한, 앞날은 모르기 때문에 프로젝트의 규모가 예상보다 더 커진다거나, 공개 프로젝트로 변경될 가능성도 생각했을 때 개인 프로젝트라도 문서화를 해둬서 손해 볼 이유는 없다.

 

 다행히도 현재 프로젝트는 인증이나 로깅 같은 초기 공통 로직들을 구현하는 시작 단계에 있어서, 이미 만들어둔 API들을 몰아서 문서화를 해야 되는 끔찍한 일은 없기에 문서화를 도입하기에 문제는 없다.

 

왜 REST Docs인가?

 스프링에서는 문서화를 위해 대표적으로 REST Docs와 Swagger를 사용한다. 하지만, 개인적으로 Swagger는 API 로직이 지저분해지고, API의 신뢰성이 보장되지 않기에 선호하지 않는다. 그래서, 기존에 사용해 봤고, 테스트 코드도 꼼꼼히 작성하면서 진행할 것이기 때문에 REST Docs를 사용하는 것으로 방향을 정했다.

 

사용해 보기

 REST Docs는 API의 테스트를 통해 요청과 응답으로 자동으로 문서 스니펫을 만들고, 해당 스니펫을 작성해 둔 아스키독 파일 양식대로 조합해 문서를 완성하기 때문에, 컨트롤러 단위 테스트에서 문서화를 진행했다.

@Test
@DisplayName("프로필 조회 성공 테스트")
void getMember() throws Exception {
    // given
    ...

    // when
    ResultActions actions = mockMvc.perform(
        get("/members")
            .header(AUTHORIZATION, TOKEN)
            .accept(APPLICATION_JSON)
    );

    // then
    actions.andDo(print())
        .andExpect(status().isOk())
        .andExpect(content().string(apiResponse));

    // restdocs
    actions.andDo(documentHandler.document(
        requestHeaders(
            headerWithName(AUTHORIZATION).description("액세스 토큰")
        ),
        responseFields(
            fieldWithPath("data.memberId").description("회원 아이디"),
            fieldWithPath("data.email").description("회원 이메일"),
            ...
            fieldWithPath("code").description("응답 코드"),
            fieldWithPath("status").description("응답 상태"),
            fieldWithPath("message").description("응답 메시지")
        )
    ));
}

 

 사용하는 방법은 크게 어렵지 않은데, 기존의 테스트 방식에 문서화를 하는 부분만 추가된다. 요청과 응답에 포함되는 헤더와 바디의 정보만 입력해주면 되는데, 오타나 잘못된 경로가 있으면 문서화가 실패하기 때문에 잘 확인하고 적어주기만 하면 된다.

 

생각보다 귀찮은 문서화

 문서화 작업을 하다 보면 요청과 응답의 정보를 일일히 적어주는 귀찮음은 어떻게 해결할 수 없지만, 문서 포맷 설정, 인코딩 설정, 기타 설정, 공통 정보 등 반복적으로 동일한 로직이 사용되는 부분들이 많다.

@MockBean(JpaMetamodelMappingContext.class)
@WebMvcTest({
	...
})
@ExtendWith({RestDocumentationExtension.class})
public class ControllerTest {
    ...
    
    // 공통 전처리
    @BeforeEach
    void setUp(WebApplicationContext context,
        final RestDocumentationContextProvider restDocumentation,
        TestInfo testInfo) {

        // 기타 공통 설정들
        ...

        documentHandler = document(
        preprocessRequest(prettyPrint()),
        preprocessResponse(prettyPrint())
        );

        DefaultMockMvcBuilder mockMvcBuilder = webAppContextSetup(context)
        .apply(documentationConfiguration(restDocumentation))
        .addFilters(new CharacterEncodingFilter("UTF-8", true));

        ...

        mockMvc = mockMvcBuilder.build();
    }
}

 

 매번 문서화 작업마다 반복되는 설정들을 위와 같이 @BeforeEach를 통해 처리하면 조금 더 간편하게 작업이 가능해진다.

actions.andDo(documentHandler.document(
    ...,
    responseFields(
        ...,
        fieldWithPath("code").description("응답 코드"),
        fieldWithPath("status").description("응답 상태"),
        fieldWithPath("message").description("응답 메시지")
    )
));

actions
    .andDo(
        documentHandler.document(
            ...,
            pageResponseFields(
                ...,
                fieldWithPath("pageInfo").description("페이지네이션 정보"),
                fieldWithPath("pageInfo.page").description("현재 페이지"),
                fieldWithPath("pageInfo.size").description("페이지 사이즈"),
                fieldWithPath("pageInfo.totalPage").description("전체 페이지 수"),
                fieldWithPath("pageInfo.totalSize").description("전체 데이터 개수"),
                fieldWithPath("pageInfo.first").description("첫 페이지 여부"),
                fieldWithPath("pageInfo.last").description("마지막 페이지 여부"),
                fieldWithPath("pageInfo.hasNext").description("다음 페이지 존재 여부"),
                fieldWithPath("pageInfo.hasPrevious").description("이전 페이지 존재 여부"),
                fieldWithPath("code").description("응답 코드"),
                fieldWithPath("status").description("응답 상태"),
                fieldWithPath("message").description("응답 메시지")
            )
        )
    );

 

 위의 코드처럼 단일 및 페이지 응답에서 매번 반복적인 로직들을 적어주고 있는데, 유틸 클래스를 만들어 분리해주면 좋다.

private static final FieldDescriptor[] pageInfoFields = new FieldDescriptor[]{
    fieldWithPath("pageInfo").description("페이지네이션 정보"),
    fieldWithPath("pageInfo.page").description("현재 페이지"),
    fieldWithPath("pageInfo.size").description("페이지 사이즈"),
    fieldWithPath("pageInfo.totalPage").description("전체 페이지 수"),
    fieldWithPath("pageInfo.totalSize").description("전체 데이터 개수"),
    fieldWithPath("pageInfo.first").description("첫 페이지 여부"),
    fieldWithPath("pageInfo.last").description("마지막 페이지 여부"),
    fieldWithPath("pageInfo.hasNext").description("다음 페이지 존재 여부"),
    fieldWithPath("pageInfo.hasPrevious").description("이전 페이지 존재 여부")
};

private static final FieldDescriptor[] responseStatusFields = new FieldDescriptor[]{
    fieldWithPath("code").description("응답 코드"),
    fieldWithPath("status").description("응답 상태"),
    fieldWithPath("message").description("응답 메시지")
};

 

 페이지 정보와 응답 상태 정보 같은 고정된 공통 값들을 필드로 만들어두고

public static ResponseFieldsSnippet pageResponseFields(FieldDescriptor... responseFields) {

    List<FieldDescriptor> allFields = new ArrayList<>();
    allFields.addAll(Arrays.asList(responseFields));
    allFields.addAll(Arrays.asList(pageInfoFields));
    allFields.addAll(Arrays.asList(responseStatusFields));

    return responseFields(allFields);
}

public static ResponseFieldsSnippet singleResponseFields(FieldDescriptor... responseFields) {

    List<FieldDescriptor> allFields = new ArrayList<>();
    allFields.addAll(Arrays.asList(responseFields));
    allFields.addAll(Arrays.asList(responseStatusFields));

    return responseFields(allFields);
}

 

 API마다 다른 응답 데이터들만 파라미터로 받아 공통 응답들과 합쳐주면 매번 같은 작업을 하는 번거로움을 줄일 수 있다.

 

마치며

 API의 신뢰성을 챙기면서 테스트 과정에서 편리하게 문서화를 할 수 있는 REST Docs지만, 그럼에도 불구하고 아직도 귀찮은 부분들이 많이 존재한다. 요청과 응답에 대해 직접 명시해주는 것도 자동화할 수 있으면 작업 속도가 더욱 향상될거 같은데, 해당 부분도 좀 더 찾아봐야 할 것 같다.

 

 글을 쓰면서도 개선할 수 있을 것 같은 부분을 발견했고, 작업을 하면서 REST Docs를 찾아보는 과정에서도 다양한 활용 사례를 많이 발견해서, 기회가 되면 다음에 더 나아진 REST Docs 활용글을 작성해보겠다. (이런 말 하면 글이 끊기던데...)

728x90

개요

 스프링을 처음 배우면 누구나 IoC와 DI에 대해서 학습을 하지만 작성자처럼 개발자가 개발에만 집중할 수 있게 하기 위해 객체의 생성과 관계 설정 같은 번거로운 작업들을 컨테이너에 떠넘기는 것 혹은 이와 비슷하게 개념적으로만 그런가 보다 하고 넘어가는 일들이 많은 거 같다. (아님 말고...)

 

 그래서 스터디에서 발표도 준비할 겸 토비의 스프링을 다시 정독하면서 DI가 무엇이고, 왜 필요한지, 적용함으로 얻을 수 있는 이득 등에 대해 최대한 간단하게 살펴보려 한다. (쓰다 보면 길어질 수도 있다)

 

 

객체 스스로 사용할 객체를 선택하고 생성하는 것이 맞을까?

 클라이언트의 주문에 대한 처리를 담당하는 주문 서비스 객체의 관심사는 무엇일까라고 생각하면 당연히 요청에 맞게 주문을 처리하는 것이다. 하지만 만약 객체 스스로 자신이 사용할 객체를 선택하고 생성한다면 이에 대한 관심사까지 담당하게 된다고 볼 수 있다.

 

 서로 다른 관심사끼리 응집되어 있다면 변경 시 서로의 변화에 영향을 받기 때문에 서로에게 영향을 주지 않기 위해 DI를 사용해 주문 서비스 객체의 핵심 관심사와 종류가 다른 관심사를 분리시킨다.

 

 

정적으로 의존 관계를 설정하면 왜 안 되는 걸까?

public class OrderService {

	private OrderRepository orderRepository = new OrderRepository();

}

public class OrderRepository {
	
}

 

 위의 경우는 DI를 적용하지 않았기 때문에 객체 스스로가 사용할 객체를 선택하고 생성하는 방식을 사용한다. 이미 엎드려 보고 물구나무서서 봐도 누구나 OrderService 객체가 OrderRepository 객체를 사용한다는 것을 알 수 있다.

 

 이런 정적인 의존 관계 설정 방식을 사용하면 설계와 코드 레벨에서 객체 간의 의존 관계가 직접적으로 드러나게 된다. 이게 왜 문제일까 생각해 보면 위에서 언급한 관심사의 분리 측면에서의 문제도 있지만, 클라이언트 객체(OrderService)가 자신이 사용할 서버 객체(OrderRepository)와 긴밀한 관계에 있게 되어 직접적으로 알고 있기 때문에 서버 객체의 변화에 따른 영향을 직접적으로 받게 된다.

 

 

동적으로 의존 관계가 설정된다면 어떨까?

 동적으로 의존 관계가 설정된다는 것은 설계나 코드 레벨에 객체 간의 실질적인 의존 관계가 직접적으로 드러나지 않고 런타임 시에 의존 관계가 설정되는 것이다. 이 부분도 설명만 들어서는 이해가 어려우니 코드로 살펴보겠다.

public class OrderService {

    private OrderRepository orderRepository;

    public OrderService(OrderRepository orderRepository) {
    	this.orderRepository = orderRepository;
    }
}

public interface OrderRepository {
	
}

public class OrderRepositoryA implements OrderRepository {
	
}

public class OrderRepositoryB implements OrderRepository {
	
}

 

 위의 코드만 보고 OrderService가 어떤 OrderRepository의 구현 클래스를 사용하는지 알 수 있을까?

 

 당연히 알 수가 없다. 필드와 생성자가 구현 클래스의 타입을 직접적으로 명시하지 않고 인터페이스를 타입으로 받고 있기 때문이다. 따라서 두 객체의 연관 관계는 설계나 코드 레벨에서는 OrderRepository의 구현 클래스 중 하나와 설정된다는 것만 알 수 있고, 런타임 시까지는 직접적인 연관 관계를 알 수 없다는 것이다.

 

 

그렇다면 외부로부터 주입받으면 무조건 DI일까?

public class OrderService {

    private OrderRepository orderRepository;

    public OrderService(OrderRepositoryA orderRepositoryA) {
    	this.orderRepository = orderRepositoryA;
    }
}

public interface OrderRepository {
	
}

public class OrderRepositoryA implements OrderRepository {
	
}

public class OrderRepositoryB implements OrderRepository {
	
}

 

 DI의 궁극적인 목표는 의존 관계를 런타임 시에 설정하는 것인데 위의 코드처럼 생성자의 파라미터에 OrderService 객체 스스로 자신이 주입받고 싶은 관계를 직접적으로 명시하고 있다면 DI라고 볼 수 없다.

 

 기존의 정적인 방식과 마찬가지로 설계와 코드 레벨에서 이미 OrderService 객체가 어떤 객체와 연관 관계를 맺을지 알 수 있기 때문이다. 하지만 DI를 의존성 주입이라는 관점에서만 바라본다면 외부로부터 의존성을 주입 받고 있긴 하니 DI라고 볼 수도 있다.

 

 그래도 DI의 가치를 생각해본다면 인터페이스를 활용해 런타임 시에 동적이 관계를 맺고 객체 간의 결합도를 낮추는 것이 객체지향적인 관점에서 더 좋다고 생각한다.

 

 

객체 간의 DI를 해주는 객체

 이쯤되면 런타임 시에 객체를 생성하고 객체들간의 의존 관계를 설정해주는 역할은 누가 수행하는지 궁금해질텐데, 이런 역할을 수행하는게 바로 많이 들어봤을 스프링 컨테이너다. 이번 글의 주제는 DI이기 때문에 컨테이너에 대해서는 나중에 자세히 다루고 지금은 넘어가겠다.

 

 

DI의 장점

 DI 자체가 지금까지 살펴봤듯이 객체지향 설계와 프로그래밍 원칙을 따른 기술이기 때문에 이들을 지켰을 때 얻을 수 있는 장점들을 자연스럽게 모두 얻을 수 있다고 보면 된다.

 

 관심사의 분리를 통해 응집도가 높은 코드를, 인터페이스를 통해 결합도가 낮은 코드를 작성할 수 있기 때문에 객체의 변경으로 인해 다른 객체에 발생하는 영향을 줄일 수 있고, 코드의 재사용성과 유연성, 유지보수성을 높일 수 있다.

 

 또한, 테스트 수행에 있어서도 대리 객체의 주입을 통해 손쉽게 외부 의존성을 대체하거나 특정 모듈이나 클래스를 고립시키고 테스트할 수 있다.

 

 

마치며

 정말 최대한 간략하게 정리하려 노력했지만 세상 일이란게 마음대로 되지 않는다. DI라는 개념 자체가 연관된 것들도 많고 워낙 복잡한 개념이다 보니 요점을 전달하면서 간략하게 적는다는게 참 쉽지 않다. 어쨋든 글을 작성하면서 개인적으로도 생각이 정리되다 보니 많은 도움이 되어 다행이다. (글을 읽는 사람들이 도움이 될진 모르겠지만...)

728x90

개요

토비의 스프링을 다시 읽으며, 개념을 자세히 정리하기보다는 면접에 나올 만한 부분을 질의응답 형식으로 정리하려고 합니다.

책을 읽는 동안 지속적으로 업데이트되는 게시글입니다.

친절하게 설명을 해주시기 위해 분량이 많아진 것이라 생각하기 때문에, 입문자도 한 번 정도는 읽어보는 것도 좋다고 생각합니다.

내용의 순서는 가나다 순이 아닌 책의 흐름에 따라 진행됩니다.

전체 글 목록은 아래 페이지에서 확인할 수 있습니다.

토비의 스프링으로 면접 준비하기

 

 

질문 및 답변

1권의 3장인 템플릿 챕터에 대한 질답 목록입니다.

 

템플릿이란

 변경이 거의 일어나지 않으며 일정한 패턴으로 유지되는 특성을 가진 부분을 자유롭게 변경되는 성질을 가진 부분으로부터 독립시켜 효과적으로 활용할 수 있도록 하는 방법이다. 즉, 고정된 작업 흐름을 가진 코드를 재사용한다는 것이다.

 

템플릿의 장점

 변하지 않지만 많은 곳에서 반복적으로 사용되는 코드와 로직에 따라 자주 확장되고 변하는 코드를 분리할 때 유용하다.

 

내부 클래스

클래스의 내부에 선언된 클래스로 주로 속해있는 클래스와 밀접한 관계에 있거나 특정 클래스나 메서드에서만 사용되는 경우 활용된다. 밀접한 관계의 클래스들을 그룹화할 수 있고, 외부로부터 강력하게 캡슐화를 할 수 있으며, 가독성을 향상할 수 있다.

 

익명 클래스

 말 그대로 이름이 없는 클래스로 나중에 다시 사용될 여지가 없는 경우 사용하는 클래스다. 일회용 클래스를 생성하지 않고 익명 클래스를 사용해 클래스의 선언과 오브젝트 생성을 합쳐 코드를 간결하게 줄일 수 있다.

 

콜백

 실행되는 것을 목적으로 다른 오브젝트의 메서드에 전달되는 오브젝트이다. 값을 참조하는 것이 목적이 아닌 특정 로직을 담은 메서드의 실행을 위해 파라미터로 전달되는 오브젝트라고 볼 수 있다.

 

템플릿/콜백 패턴

 

728x90

개요

토비의 스프링을 다시 읽으며, 개념을 자세히 정리하기보다는 면접에 나올 만한 부분을 질의응답 형식으로 정리하려고 합니다.

책을 읽는 동안 지속적으로 업데이트되는 게시글입니다.

친절하게 설명을 해주시기 위해 분량이 많아진 것이라 생각하기 때문에, 입문자도 한 번 정도는 읽어보는 것도 좋다고 생각합니다.

내용의 순서는 가나다 순이 아닌 책의 흐름에 따라 진행됩니다.

전체 글 목록은 아래 페이지에서 확인할 수 있습니다.

토비의 스프링으로 면접 준비하기

 

 

질문 및 답변

1권의 2장인 테스트 챕터에 대한 질답 목록입니다.

테스트의 가치와 장점, 활용 전략, 스프링과의 관계에 대해 질답입니다.

 

테스트의 유용성

 테스트는  코드가 예상하고 의도한 대로 정확히 동작하는지 확인하여 확신할 수 있게 해주는 작업으로, 이를 통해 설계나 코드의 결함을 발견하고 제거할 수 있다.

 

웹을 통한 테스트 방식의 문제점

 관련된 모든 기능을 계층별로 구현한 후에 웹을 통해 요청을 보내 예상대로 동작하는지 확인해야 하기 때문에 도중에 테스트가 불가능하고, 모든 기능을 구현한 후에 문제를 발견하면 어느 부분에서 문제가 발생했는지 파악하기 힘들다. 이는 결국 하나의 테스트를 위해 참여하는 클래스와 코드가 많기 때문에 테스트의 결과가 다른 계층의 코드와 컴포넌트 등에 영향을 많이 받고 번거로움이 생기는 문제로 이어진다.

 

테스트의 단위가 작아야 하는 이유

 많은 것을 한 번에 테스트를 하기 위해서는 과정도 복잡해지고 문제의 원인을 찾기 힘들어진다. 따라서 테스트도 관심사의 분리를 적용해 관심이 다른 테스트는 분리하여 작은 단위로 테스트하는 것이 좋다. 이렇게 작은 단위의 테스트는 큰 단위의 테스트를 진행할 때 발생한 문제의 원인을 찾기도 더욱 쉽게 만들어 준다.

 

단위 테스트란

 작은 단위의 코드에 대해 테스트를 수행하는 것으로 정확한 단위나 크기와 범위가 정해진 것은 아니지만 하나의 관심에 집중해서 효율적으로 테스트할 수 있는 범위에 대한 테스트다.

 

테스트 주도 개발(TDD, Test Driven Development)

 만들고자 하는 기능의 내용과 코드를 검증할 수 있는 테스트 코드를 먼저 작성한 후에 테스트가 성공할 수 있게 코드를 작성하고 리팩토링을 거치는 하나의 사이클을 반복하며 기능을 완성시켜 나가는 방식의 개발 방법이다.

 

TDD의 장점

 테스트를 생성한 후에 코드를 완성해 나가기 때문에 테스트를 생략하지 않고 꼼꼼하게 개발을 진행할 수 있고, 테스트를 작성하는 시간과 로직 작성의 시간 간격이 짧아 피드백을 빠르게 받을 수 있다. 또한 작은 기능 단위로 테스트를 진행하며 기능을 완성하기 때문에 자연스럽게 단위 테스트를 수행할 수 있다.

 

Fixture

 테스트를 수행하는 데 필요한 정보다 오브젝트이다. 일반적으로 테스트에서 반복적으로 사용되기 때문에 중복을 제거하기 위해 추출해서 사용한다.

 

테스트와 애플리케이션 컨텍스트의 관계

 애플리케이션 컨텍스트가 생성될 때는 모든 싱글톤 빈 오브젝트를 초기화하기 때문에 빈이 많아지고 복잡한 애플리케이션일수록 컨텍스트 생성에 큰 비용이 소모된다. 애플리케이션 컨텍스트는 초기화 이후에는 내부 상태가 바뀌는 일이 거의 없고, 빈은 싱글톤이라 상태를 가지지 않기 때문에 여러 테스트가 공유해서 사용해도 문제가 없다.

 스프링은 이런 문제를 해결하기 위해 환경이 같은 테스트가 하나의 애플리케이션 컨텍스트를 공유해서 사용할 수 있게 지원한다. 

 

DI는 왜 필요한가

 구현 클래스가 하나이더라도 인터페이스를 통한 DI는 사용해야 한다. 소프트웨어 개발에서 변하지 않는 것은 없고, 부가적인 기능을 추가하기 쉬워지며, 가능한 작은 단위의 대상에 대해 독립적으로 만들어지고 실행되게 하기 때문에 테스트에 유용하다.

 

스프링에서 테스트를 하기 위해서는 컨텍스트가 필수일까?

 스프링 API를 직접 사용하지 않는 코드를 테스트한다면 굳이 무겁게 컨텍스트를 생성하거나 사용할 필요가 없다.

728x90

개요

토비의 스프링을 다시 읽으며, 개념을 자세히 정리하기보다는 면접에 나올 만한 부분을 질의응답 형식으로 정리하려고 합니다.

친절하게 설명을 해주시기 위해 분량이 많아진 것이라 생각하기 때문에, 입문자도 한 번 정도는 읽어보는 것도 좋다고 생각합니다.

내용의 순서는 가나다 순이 아닌 책의 흐름에 따라 진행됩니다.

 

 

목차

[토비의 스프링으로 면접 준비하기] 1장:오브젝트와 의존관계

[토비의 스프링으로 면접 준비하기] 2장:테스트

[토비의 스프링으로 면접 준비하기] 3장:템플릿

 

 

728x90

문제

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	private String name;
	
	private String email;
}

 

위의 경우에는 name과 email 필드는 그저 평범한 문자열 데이터를 저장하는 역할만 수행하고

데이터의 검증이나 추가적인 처리는 포함하지 않고 있기 때문에 이를 위한 로직이 필요하다.

 

그렇다면 이러한 로직은 어디에 위치하는 것이 좋을지 생각해 보면

당장은 엔티티에 추가하는 것만 생각이 난다.

 

하지만 name과 email에 대한 로직이 다른 영역에 존재하는 것이 어울리는지 생각해 보면 그렇지 않다.

그러면 각 필드를 의미 있는 객체로 만들 수 있다면 이러한 부분을 해결할 수 있지 않을까?

 

 

VO(Value Object)란?

class Address {
    private String city;
    private String street;
    private String zipcode;
}

 

VO는 간단하게 하나 또는 그 이상의 속성들을 묶어 값 자체를 나타내는 객체라고 볼 수 있다.

 

엔티티 같은 경우야 동일성을 보장받지만 VO는 동일성을 보장받지 않기 때문에

equals와 hashCode 메서드를 오버라이딩 하여 값 자체를 비교하는 기준을 재정의 해줘야 한다.

 

이와 같은 이유 때문에 VO는 값이 변하면 동일하지도 동등하지도 못한 객체가 돼버리기 때문에

값이 변해서는 안 되는 수정자가 없는 불변 객체여야만 한다.

 

따라서 VO는 생성자를 통해 처음 생성된 이후에는 변해서는 안된다.

 

 

왜 원시 타입이 아닌 VO를 사용해야 할까?

VO를 사용하지 않고 원시 타입을 사용하는 경우에는 엔티티 자체에

해당 값들에 대한 로직들이 포함되게 되어 지나치게 복잡해지고 가독성도 떨어진다.

 

하지만 VO를 사용한다면 해당 값들에 대한 로직이 모두 엔티티에서 분리되어

있어야 할 곳으로 응집되게 되고 좀 더 객체 지향적인 프로그래밍이 가능하다.

 

 

VO를 엔티티에 매핑하는 방법

JPA는 임베디드 타입을 제공하여 VO와 같은 복합적인 값을 매핑할 수 있고

이를 이용해 기존의 name 필드를 VO로 바꿔보겠다.

@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode
@Getter
public class Name {

	public static final int MIN_LENGTH = 2;
	public static final int MAX_LENGTH = 10;

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

	public Name(final String value) {
		validateNull(value);
		final String trimmedValue = value.trim();
		validateBlank(trimmedValue);
		validateLength(trimmedValue);
		this.value = trimmedValue;
	}

	private void validateNull(final String value) {
		if (value == null) {
			throw new NullPointerException("이름은 Null일 수 없습니다.");
		}
	}

	private void validateLength(final String value) {
		if (value.length() < MIN_LENGTH) {
			throw new RuntimeException("2글자 이상이여야 합니다.");
		}

		if (value.length() > MAX_LENGTH) {
			throw new RuntimeException("10글자 이하여야 합니다.");
		}
	}

	private void validateBlank(final String value) {
		if (value.isBlank()) {
			throw new RuntimeException("이름은 공백일 수 없습니다.");
		}
	}
}

 

@Embeddable 어노테이션을 클래스 레벨에 추가하면

해당 클래스를 임베디드 타입으로 사용할 수 있게 만들 수 있다.

 

생성자로 생성된 이후에는 불변 객체로 존재해야 하기 때문에

생성할 때 모든 조건을 검증한 후에 객체를 생성해 준다.

 

또한 setter를 제외한 getter와 검증 등의 로직도

모두 엔티티가 아닌 VO에 작성해 준다.

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@Embedded
	private Name name;

	@Embedded
	private Email email;
}

 

엔티티에서는 @Embedded 어노테이션을 필드에 추가해

임베디드 타입을 매핑할 수 있다.

 

이로서 엔티티에는 값과 관련된 로직들이 존재하지 않게 되어

복잡하지 않고 가독성이 좋아졌다.

728x90

개요

토비의 스프링을 다시 읽으며, 개념을 자세히 정리하기보다는 면접에 나올 만한 부분을 질의응답 형식으로 정리하려고 합니다.

책을 읽는 동안 지속적으로 업데이트되는 게시글입니다.

친절하게 설명을 해주시기 위해 분량이 많아진 것이라 생각하기 때문에, 입문자도 한 번 정도는 읽어보는 것도 좋다고 생각합니다.

내용의 순서는 가나다 순이 아닌 책의 흐름에 따라 진행됩니다.

전체 글 목록은 아래 페이지에서 확인할 수 있습니다.

토비의 스프링으로 면접 준비하기

 

 

질문 및 답변

1권의 1장인 오브젝트와 의존관계 챕터에 대한 질답 목록입니다.

객체지향의 주요 개념과 디자인 패턴 그리고 IoC와 DI의 필요성과 원리 등에 대한 질답입니다.

 

관심사의 분리 원칙(Separation of Concerns, SoC)

 변화는 보통 집중된 한 가지 관심에 대해 일어나지만 그에 따른 작업은 한곳에 집중되지 않는 경우가 많다. 관심사의 분리는 관심이 같은 것끼리 하나의 객체 또는 가까운 객체로 모아, 관심이 다른 것과는 가능한 떨어져 서로 영향을 주지 않도록 분리하는 것이다. 이를 통해 변경이 일어날 때 필요한 작업을 최소화할 수 있고, 그러한 변경이 다른 곳에 문제를 일으키지 않게 할 수 있다.

 

리팩토링

 기존의 코드를 기능의 동작방식에는 변화 없이 내부 구조만 변경하여 재구성하는 작업으로, 코드의 내부 설계를 개선하여 코드를 이해하기 쉽게 만들고, 변화에 대응하기 쉽게 할 수 있다. 이를 통해 코드의 품질과 생산성을 향상하고, 유지보수가 편리해지는 견고하면서도 유연한 개발을 할 수 있다.

 

메서드 추출

 공통의 기능을 담당하는 메서드로 중복된 코드를 추출하는 기법이다.

 

상속과 확장

 상속을 통해 자식 클래스가 부모 클래스의 자원을 물려받아, 부모 클래스와 다른 부분만 추가하거나 재정의하여 기존 코드를 쉽게 확장할 수 있다. 하지만 이렇게 코드의 재사용만을 위한 기법이 아닌 더 구체적인 클래스를 구현하기 위해 사용하는 것이 주목적이다.

 

템플릿 메서드 패턴

 상속을 통해 슈퍼 클래스의 기능을 확장할 때 사용하는 가장 대표적인 디자인 패턴이다. 변하지 않는 기능을 슈퍼 클래스에 구현하고 자주 변경되고 확장되는 기능은 서브 클래스에 만드는 방식으로, 이러한 부분을 추상 메서드 혹은 오버라이드가 가능한 메서드로 정의하여 템플릿처럼 사용한다.

 

팩토리 메서드 패턴

 오브젝트를 만드는 팩토리 클래스를 사용하여 슈퍼 클래스는 구체적인 오브젝트에 대해서는 알지 못하게 하는 디자인 패턴이다. 클래스의 생성과 사용을 분리하여 결합도를 낮추고, 캡슐화를 통해 정보를 은닉 처리할 수 있다는 장점이 있다.

 

 슈퍼 클래스는 오브젝트의 생성에 대해 추상화를 하고, 이를 서브 클래스가 어떤 오브젝트를 생성할지 결정하기 때문에 슈퍼 클래스는 추상화된 오브젝트만 알고 있게 된다.

 

디자인 패턴의 개념과 장점

 소프트웨어 설계 시 특정 상황에서 자주 만나는 문제를 해결하기 위해 사용할 수 있는 재사용 가능한 설루션이다. 다양한 디자인 패턴에 대해 이해하고 있다면 문제를 마주치더라도 쉽게 효율적인 설루션을 생각할 수 있고, 자신과 상대가 모두 디자인 패턴에 대해 알고 있다면 의사소통을 더욱 간결하게 할 수 있다.

 

상속의 단점

 자바는 다중 상속을 허용하지 않기 때문에 이미 상속을 받은 클래스에 추가적인 상속이 필요한 경우에는 상속을 적용하기 힘들며, 관심사를 분리하고 확장성이 유용하게 만들어도 상속을 통해 밀접한 관계가 될 수밖에 없다는 단점이 있다.

 

인터페이스를 사용하는 이유

 인터페이스를 사용하면 객체 입장에서는 자신이 사용하는 클래스에 대해 직접적으로 알지 못하게 된다. 즉, 인터페이스는 사용하는 클래스와 실제 사용되는 클래스의 사이에서 두 클래스의 긴밀한 관계를 느슨하게 해 준다. 결국 사용하는 입장에서는 인터페이스에 정의된 기능에만 관심을 가지기 때문에 실제로 구현된 내용에는 관심을 가지지 않게 된다. 따라서 사용하고자 하는 클래스가 바뀌어도 아무런 문제가 생기지 않는다.

 

의존관계를 직접 설정하지 않는 이유

 객체 자신이 사용할 오브젝트를 직접 생성하고 선택하는 것도 하나의 관심사라고 볼 수 있다. 이러한 관심사를 분리하기 위해 객체 자신을 사용하는 클라이언트 객체에 위임하여 런타임 시에 오브젝트와 오브젝트 사이의 의존(사용) 관계를 설정하게 한다.

 

개방 폐쇄 원칙(OCP : Open-Closed Principle)

 확장에 대해서는 개방적이고 수정에 대해서는 폐쇄적이어야 한다는 원칙으로, 기존의 코드에 영향을 주지 않고 기능을 추가할 수 있도록 설계해야 하는 것을 의미한다. 예를 들면 인터페이스를 통해 기능에 대한 확장은 개방하고, 이를 이용하는 클래스는 변화에 영향을 받지 않게 폐쇄할 수 있다.

 즉, 변화의 특성이 다른 부분을 구분해주고, 각각 다른 목적과 다른 이유에 의해 다른 시점에 독립적으로 변경될 수 있는 효율적인 구조를 만들어주는 것이다.

 

객체지향 설계 원칙 (SOLID)

 SRP(Single Responsibility Principle:단일 책임 원칙), OCP(Open-Closed Principle:개방 폐쇄 원칙), LSP(Liskov substitution principle:리스코프 치환 원칙), ISP((Interface segregation principle:인터페이스 분리 원칙), DIP(Dependency inversion principle:의존관계 역전 원칙) 총 5가지의 객체지향 설계 원칙을 의미한다. 특정한 문제에 대한 솔루션인 디자인 패턴과 다르게 더 일반적인 상황에서 적용 가능한 객체지향 설계의 기준이라고 볼 수 있다.

 

높은 응집도와 낮은 결합도

 응집도는 하나의 모듈 혹은 클래스에 있는 기능들의 연관도로, 응집도가 높다면 하나의 관심사에만 집중되어 있기 때문에 변경이 필요할 때 특정 모듈만 수정하면 해결할 수 있다. 결합도는 관심이 다른 모듈 혹은 클래스와의 의존성의 정도로, 결합도가 낮다면 느슨하게 연결되어 있어 독립적이기 때문에 변경에 대한 여파가 전파되지 않는다.

 

전략 패턴

 기능에서 변경이 필요한 알고리즘을 인터페이스로 분리시켜 이를 구현한 구체적인 알고리즘 클래스를 런타임시에 필요에 따라 바꿔서 적용할 수 있는 행위 디자인 패턴으로, 알고리즘의 변형이 빈번한 경우에 유용하다. 추상화된 전략 인터페이스와 이를 구현한 클래스, 전략을 등록하고 실행하는 컨텍스트와 전략을 설정하고 결과를 얻는 클라이언트가 있다.

 

스프링이란

 객체지향적 설계와 디자인 패턴의 장점을 개발자들이 자연스럽게 활용할 수 있게 해주는 프레임워크다. IoC를 극한까지 적용하고 있는 IoC 프레임워크이기 때문에 컴포넌트의 생성과 관계설정, 사용, 생명주기 관리 등을 위해 애플리케이션 전반에 걸쳐 IoC를 적용하기 편리하게 도와준다.

 오브젝트가 어떻게 설계되고, 만들어지고, 관계를 맺고 사용되는지에 관심을 맞춰 개발된 프레임워크다. 개발자가 이러한 과정들을 설계하고 작업할 때 반복적으로나 강제적으로 해야하는 번거로운 작업들을 편하게 할 수 있도록 지원해준다.

 

오브젝트 팩토리

 객체의 생성 방법을 결정하고 만들어진 오브젝트를 돌려주는 오브젝트로 디자인 패턴에서 사용되는 팩토리와는 다르다. 단순하게 오브젝트를 생성하는 쪽과 생성된 오브젝트를 사용하는 쪽의 역할과 책임을 분리하려는 목적으로 사용한다.

 

IoC(Inversion of Control)

 오브젝트 스스로 자신이 사용할 클래스를 결정하고, 생성하는 것처럼 사용하는 쪽에서 제어하는 것이 아닌, 반대로 모든 제어 권한을 자신이 아닌 다른 오브젝트에게 위임하는 것이다. 이렇게 되면 오브젝트는 자신이 사용할 오브젝트를 스스로 결정하지 않고, 생성하지도 않으며, 자신도 어떻게 만들어지고 사용되는지 알 수 없다. 그래서 IoC를 적용하다 보면 자연스럽게 관심이 분리되고 책임이 나눠져 설계가 깔끔해지고, 유연성과 확장성이 향상된다.

 

 추가로 프레임워크도 제어의 역전이 적용된 기술이라고 볼 수 있다. 개발자가 프레임워크를 사용하여 개발하는 것이 아닌 프레임워크에 맞춰 개발하기 때문이다.

 

스프링에서의 빈이란

 스프링 컨테이너가 생성과 관계설정, 사용 등을 제어해 주는 IoC가 적용된 오브젝트 단위의 컴포넌트이다. 스프링에서 사용된다고 모두 빈인 것이 아닌, 스프링 컨테이너에 의해 제어되는 것만을 빈이라고 한다.

 

빈 팩토리와 애플리케이션 컨텍스트의 차이점

 빈 팩토리는 빈을 생성하고 관계를 설정하는 IoC의 기본적인 부분을 제어한다면 애플리케이션 컨텍스트는 애플리케이션 전반의 모든 구성요소의 제어 작업을 담당한다. 애플리케이션 컨텍스트는 BeanFactory 인터페이스를 상속했기 때문에 빈 팩토리라고 볼 수도 있다.

 

@Configuration

 빈들을 애플리케이션 컨텍스트에 등록하기 위해 사용하는 애너테이션으로 클래스에 해당 어노테이션을 설정하면 해당 클래스의 본문을 파싱해 @Bean 애너테이션이 설정된 메서드의 이름으로 빈을 등록한다. 이때 등록된 빈은 스프링 컨테이너에 의해 관리되고 싱글톤임이 보장된다.

 

애플리케이션 컨텍스트의 개념과 동작방식 및 장점

 애플리케이션 컨텍스트는 애플리케이션에서 IoC를 적용해 관리할 모든  오브젝트에 대한 생성과 관계설정 등을 담당한다. 애플리케이션 컨텍스트는 클라이언트가 빈을 요청하면 빈 목록에 해당 빈이 있는지 확인하여 빈을 생성하게 한 후에 클라이언트에게 전달한다. 이렇게 클라이언트는 구체적인 오브젝트의 생성 과정을 알 필요가 없기 때문에 일관된 방식으로 원하는 오브젝트를 가져올 수 있게 된다. 추가로 생성과 관계설정만이 아닌 오브젝트의 생성 방식, 시점, 전략, 후처리 등 다양한 기능을 제공하기도 한다.

 

동일성과 동등성

 동일한 오브젝트라면 서로 같은 객체의 주소가 동일한 것이고, 동등한 오브젝트라면 서로 다른 객체지만 객체의 동등성 기준에 따라 동등한 객체인 것을 말한다.

 

스프링에서 싱글톤을 사용하는 이유

 클라이언트의 요청마다 필요한 오브젝트를 새로 만든다면 서버에 부하가 생길 것이고 감당하기 힘들어 진다. 이러한 문제를 해결하기 위해 애플리케이션 안에 제한된 수의 오브젝트를 만들어서 사용하기 위한 것이 싱글톤 패턴의 원리이고, 따라서 스프링은 싱글톤을 사용한다.

 

싱글톤 패턴과 구현

 클래스를 애플리케이션 내에서 제한된 인스턴스 개수나 보통 하나만 존재하도록 강제하는 디자인 패턴이다. 이렇게 생성된 인스턴스는 애플리케이션 내에서 전역적으로 접근이 가능하고, 주로 애플리케이션의 여러 곳에서 공유하는 경우에 사용된다.

 클래스 내에서 오브젝트를 생성하지 못하게 생성자를 private으로 제한하고, 싱글톤 오브젝트를 저장할 스태틱 필드를 정의하여 팩토리 메서드를 사용해 한 번만 오브젝트를 생성하게 하여 필드에 저장한다. 그 후에는 이미 만들어져 스태틱 필드에 저장된 오브젝트를 넘겨줘 공유한다.

 

싱글톤 패턴의 한계

 private 생성자 하나만 가지고 있기 때문에 다른 생성자가 없어 상속을 할 수 없고 다형성도 사용할 수 없다. 테스트에서도 만들어지는 방식이 제한되기 때문에 목 오브젝트 등으로 대체하기 힘들고, 자원을 공유하여 격리된 환경에서의 테스트가 힘들다. 또한, 클래스 로더가 두 개 이상일 경우 지정하지 않으면 싱글톤이 하나만 만들어지는 것이 보장되지 않고, 전역 상태로 사용되기 때문에 어떤 객체에서든 접근할 수 있다는 위험이 있다.

 

싱글톤 레지스트리

 스프링에서의 싱글톤은 디자인 패턴의 싱글톤 패턴과 비슷하지만 구현 방법은 많이 다르다. 기존 싱글톤 패턴의 한계를 극복하기 위해 스프링은 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 싱글톤 레지스트리 기능을 제공한다. 이는 오브젝트 생성에 대한 모든 권한이 애플리케이션 컨텍스트가 가지고 있기 때문에 싱글톤을 생성하고, 관리하고, 공급할 수 있기 때문에 가능한데, 이로 인해 스프링에서는 평범한 자바 클래스도 싱글톤으로 활용할 수 있다.

 

싱글톤을 사용할 때 주의해야 할 점

 멀티 스레드 환경에서 하나의 싱글톤 오브젝트를 여러 스레드가 동시에 사용할 수 있기 때문에 내부에 상태를 저장하기 위해 읽기 전용이 아닌 인스턴스 변수를 수정하여 상태를 공유하는 것은 위험하다. 따라서 파라미터나 지역 변수, 리턴 값 등을 이용해 각 스레드마다 독립적인 공간을 활용해야 한다.

 

DI(Dependency Injection, 의존관계 주입)

 오브젝트 레퍼런스를 외부로부터 주입 받아 이를 통해 다른 오브젝트와 런타임 시에 의존관계가 만들어지는 것이다.

 

의존관계

 A가 B를 의존하고 있다는 것은 A의 의존대상인 B가 변하면 A에도 영향을 미친다는 것이고 반대로 B는 A에 의존하지 않고 있기 때문에 A의 변화는 B에 영향을 끼치지 않는다.

 

런타임 의존관계

 설계와 코드에 드러나지 않는 실제 사용대상인 구체적인 의존 오브젝트와의 관계가 런타임 시에 설정되어 드러나는 것을 말한다. 그리고 이러한 의존관계를 런타임 시에 설정해 주는 제3의 존재가 애플리케이션 컨텍스트다.

 

DL(Dependency Lookup, 의존관계 검색)

 스프링은 의존관계를 맺는 방법이 외부로부터의 주입받는 의존관계 주입 말고도 스스로 의존관계 검색을 통해 자신이 필요로 하는 의존 오브젝트를 능동적으로 찾는 방법도 존재한다. 이 방법은 의존관계를 맺을 오브젝트를 결정하거나 생성하는 것은 스프링 컨테이너에게 권한을 넘기지만, 가져올 때는 컨테이너에게 요청하는 방법을 사용한다.

 

DI와 DL의 차이

 DI는 오브젝트와 오브젝트 사이의 의존관계를 설정하기 위해서는 컨테이너가 생성과 초기화 권한을 갖고 있어야 주입해 줄 수 있기 때문에 두 오브젝트 모두 빈으로 등록이 되어 있어야 한다. 하지만 DL은 검색을 하려는 대상 오브젝트만 빈이면 검색하는 오브젝트 자신이 빈일 필요가 없다.

 

외부로부터 레퍼런스를 주입받으면 모두 DI일까?

 레퍼런스를 주입받는다고 모두 DI는 아니다. DI는 런타임 시에 의존관계가 결정되어 주입되어야 하는데 이미 파라미터의 타입으로 구체적인 클래스 타입이 명시되어 있다면 DI를 받는다고 할 수 없다.

 

DI의 장점

 객체지향 설계와 프로그래밍 원칙을 따른 기술이기 때문에 구체적인 의존관계가 드러나지 않고, 인터페이스를 통해 결합도를 낮추고, 의존관계의 대상이 변경에 영향을 받지 않으며, 확장에는 자유로워 진다.

 이로 인해 기능 구현의 교환이 자유롭고(예:기존에 사용하던 DB에서 다른 DB로의 교환), 기존코드의 변경 없이 새로운 런타임 의존관계를 설정하여 부가기능도 자유롭게 추가할 수 있다.

 

생성자를 이용한 의존관계 주입

 생성자에 파라미터를 만들어 이를 통해 스프링 컨테이너가 의존할 오브젝트 레퍼런스를 넘겨주는 방식이다. 한 번에 필요한 모든 파라미터를 다 받아야 한다.

 

메서드를 이용한 의존관계 주입

 수정자 메서드와 초기화 메서드를 이용해서도 의존관계를 주입할 수 있다. 생성자를 이용한 의존관계 주입과는 다르게 한 번에 모든 파라미터를 다 받을 필요 없이 필요에 따라 받아서 주입할 수 있다. 수정자 메서드 방식은 한 번에 하나의 파라미터만 받아야 한다는 단점이 있고, 초기화 메서드 방식은 파라미터 수의 제한이 없다. 주로 자바빈 규약을 따르는 수정자 메서드 방식을 사용한다. 

728x90

Mapped Diagnostic Context

멀티 쓰레드 환경에서 로그가 뒤섞이는 문제를 해결하기 위해 사용할 수 있는 객체로

현재 실행 중인 쓰레드에 메타 정보를 넣고 관리하는 공간이다.

 

  • 내부적으로 쓰레드 로컬을 사용한다.
  • Map 형태로 구성되어 (키, 값) 형태로 값을 저장하고 사용할 수 있다.
  • 다양한 데이터를 저장해 구체적이고 유용한 로그 메시지를 제공할 수 있다.

 

주로 각각의 요청에 대한 로깅을 구분하기 위해 고유한 요청 ID를 설정하거나

사용자 정보, 세션, 트랜잭션, 로깅 수준 등을 설정한다.

 

 

MDC 사용

implementation 'org.springframework.boot:spring-boot-starter-logging'

 

위의 디펜던시를 추가하면 org.slf4j 패키지에 있는 MDC를 사용할 수 있다.

 

위에서 언급한 것처럼 Map 형태로 이루어져 있기 때문에 put, get, remove, clear 처럼

자바의 유틸 클래스에 있는 Map의 메서드와 다를게 없어서 사용에 어려움은 없다.

 

이제 MDC를 어디에서 설정해주는게 효율적인지를 생각해 봐야 하는데

요청에 대한 모든 로그를 남기려면 가장 처음에 접근하는 필터단에서 처리해주는게 효율적이다.

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class MDCLoggingFilter implements Filter {

	@Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        final UUID uuid = UUID.randomUUID();
        MDC.put("request_id", uuid.toString());
        chain.doFilter(request, response);
        MDC.clear();
    }
}

 

위와 같이 @Order 어노테이션을 사용해 filter 중에서도 가장 빠른 순서로 만들어준다.

 

랜덤으로 고유한 UUID를 만들어 MDC에 저장해주면 요청별로 고유 ID를 기록할 수 있게 되고

쓰레드 로컬 변수는 초기화하지 않으면 쓰레드풀에서 다시 꺼낼 때

이전 데이터가 남아있게 되어 doFilter가 끝난 후에는 MDC를 clear 해줘야 한다.

 

 

Logback 적용

<configuration>
    <appender name="CONSOLE_APPENDER" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <charset>utf8</charset>
            <pattern>%date{HH:mm:ss.SSS, Asia/Seoul} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    <root level="INFO">
        <appender-ref ref="CONSOLE_APPENDER" />
    </root>
</configuration>

 

위와 같이 설정하면 콘솔창에 로그가 출력되는 것을 확인할 수 있다.

 

AOP를 적용하여 번거로운 로깅 처리를 간단하게 할 수도 있고

Spring Cloud Sleuth와 Zipkin을 사용해 분산된 마이크로서비스 간의 로깅도 가능하다 하니

이 부분들도 학습하면서 언젠가 정리해보도록 하겠다.

+ Recent posts