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

QClass

// import static com.~.~.~.~.QCustomer.*;
QCustomer customer = QCustomer.customer;
QCustomer customer = new QCustomer("myCustomer");

 

실제 도메인 클래스의 질의 타입으로 정적으로 사용하거나 별칭을 붙여 사용할 수 있다.

 

쿼리 사용해 보기

JPAQuery<?> query = new JPAQuery<Void>(entityManager);
HibernateQuery<?> query = new HibernateQuery<Void>(session);

 

JPA와 Hibernate API를 모두 지원하기 때문에 위와 같이 둘 다 사용 가능하고

두 클래스 모두 JPQLQuery 인터페이스의 구현체다.

QCustomer customer = QCustomer.customer;
Customer bob = queryFactory.selectFrom(customer)
  .where(customer.firstName.eq("Bob"))
  .fetchOne();

 

두 쿼리 인스턴스 모두 주로 JPAQueryFactory(혹은 Hibernate)를 통해서 생성할 수 있다.

위의 코드의 과정을 간략하게 정리하면

쿼리 팩토리로.(지정한 컬럼과 테이블의 결과를 하나만 반환하는) 쿼리를 만든다.

라고 할 수 있다.

select customer from Customer as customer
where customer.firstName = "Bob" or customer.lastName = "Wilson"

 

예를 들어 위의 쿼리문을 QueryDSL로 작성하면

queryFactory.selectFrom(customer)
    .where(customer.firstName.eq("Bob").or(customer.lastName.eq("Wilson")));

 

이러한 코드를 작성할 수 있는데 실제 쿼리문과 유사한 형태에 IDE를 통한 자동완성과

문법 체크의 도움을 받아 편리하고 안정적이게 작성할 수 있게 된다.

 

조인

QueryDSL도 마찬가지로 JPQL에 있는 다양한 조인들을 지원한다.

select cat from Cat as cat
inner join cat.mate as mate
left outer join cat.kittens as kitten

 

예를 들어 위와 같이 동일한 테이블을 조인한다고 했을 때는

QCat cat = QCat.cat;
QCat mate = new QCat("mate");
QCat kitten = new QCat("kitten");

 

테이블의 별칭을 다르게 만들고

queryFactory.selectFrom(cat)
    .innerJoin(cat.mate, mate)
    .leftJoin(cat.kittens, kitten)
    .fetch();

 

서로 같은 컬럼을 기준으로 조인을 하는 경우에는 mySQL의 using처럼

on절을 사용하지 않고도 조인을 할 수 있고

별도의 조건으로 조인을 하는 경우에는 다음과 같이 on절을 사용하면 된다.

queryFactory.selectFrom(cat)
    .leftJoin(cat.kittens, kitten)
    .on(kitten.bodyWeight.gt(10.0))
    .fetch();

 

일반적인 사용

  • select : 쿼리의 프로젝션을 설정한다.
  • from : 쿼리의 소스를 추가한다.
  • join : innerJoin, join, leftJoin, rightJoin, on을 사용하여 조인 요소를 추가할 수 있고, 조인 메서드의 첫 번째 파라미터는 원본, 두 번째 파라미터는 조인 대상이 된다.
  • where : 쿼리의 필터를 추가한다. 쉼표 혹은 and 연산자를 통해 계단식으로 배열한다.
  • groupBy : 그룹화 조건으로 사용할 파라미터를 추가한다.
  • having : 그룹화 대상이 갖고 있어야 할 파라미터를 추가한다.
  • orderBy : 정렬 기준으로 사용할 파라미터를 추가한다.
  • limit, offset, restrict : 페이징의 결과를 설정한다.

 

정렬

QCustomer customer = QCustomer.customer;
queryFactory.selectFrom(customer)
    .orderBy(customer.lastName.asc(), customer.firstName.desc())
    .fetch();

 

orderBy 메서드의 파라미터로 정렬 기준이 될 컬럼과 정렬 방식을 전달해 주면 된다.

 

그룹화

queryFactory.select(customer.lastName).from(customer)
    .groupBy(customer.lastName)
    .fetch();

 

그룹화도 마찬가지로 groupBy 메서드의 파라미터로 기준을 전달해 주면 된다.

 

삭제절

QCustomer customer = QCustomer.customer;
queryFactory.delete(customer).execute();
queryFactory.delete(customer).where(customer.level.lt(3)).execute();

 

데이터의 삭제는 delete - (where) - execute 형식으로 이루어져 있는데

where 절을 생략하면 해당 테이블의 모든 데이터를 삭제하는

JPA로 치면 deleteAll 메서드에 해당한다.

 

execute가 호출되면서 실제 데이터가 삭제되고 삭제된 엔티티의 수를 반환한다.

 

JPA의 DML 절은 JPA 레벨의 영속성 전파 규칙을 따르지 않고, 2차 레벨 캐시와 연동되지 않는다.

 

수정절

QCustomer customer = QCustomer.customer;
queryFactory.update(customer).where(customer.name.eq("Bob"))
    .set(customer.name, "Bobby")
    .execute();

 

수정도 비슷하게 update - (where) - set - execute 형식으로 이루어져 있다.

 

마찬가지로, JPA의 DML 절은 JPA 레벨의 영속성 전파 규칙을 따르지 않고, 2차 레벨 캐시와 연동되지 않는다.

 

서브쿼리

QEmployee employee = QEmployee.employee;
QEmployee e = new QEmployee("e");
queryFactory.selectFrom(employee)
    .where(employee.weeklyhours.gt(
        JPAExpressions.select(e.weeklyhours.avg())
            .from(employee.department.employees, e)
            .where(e.manager.eq(employee.manager))))
    .fetch();

 

서브쿼리는 정적 팩토리 메서드인 JPAExpression를 통해 사용할 수 있다.

기존 방식과 마찬가지로 select, from, where절 등을 활용하여 서브쿼리를 작성하면 된다.

 

쿼리 내보내기

Query jpaQuery = queryFactory.selectFrom(employee).createQuery();
// 쿼리 조정 및 최적화 등
List results = jpaQuery.getResultList();

 

createQuery 메서드를 사용해 쿼리문을 생성할 수 있고, 이 작업은 실제로 쿼리를 날리는 것이 아니라

쿼리문을 만들어두기만 한 상태이기 때문에 추가적인 작업을 거친 후에

getResultList 같은 메서드를 호출하여 완성된 쿼리를 실제로 날려서 데이터를 조회할 수 있다.

728x90

개요

우선 처음 사용해보고 느낀 점은 초기 셋팅할 때 햇갈린거만 빼면 생각보다 어렵지 않다는 것이다.

 

워낙 유명한 툴이다 보니 인터넷에 정보도 많고 다양한 플러그인과 GUI 덕분에 처음 사용해보는 사용자도

어려움 없이 사용할 수 있는거 같다. 물론 어느 정도 스크립트를 작성하긴 해야한다.

 

아마 이전에 Github Actions를 사용해본 사람이라면 스크립트를 짜는 것이 어렵진 않을거다.

 

CI/CD Pipeline

당장은 EC2 인스턴스는 Jenkins 서버용과 SpringBoot 서버용으로 두 개만 사용 중이고

프로젝트가 어느 정도 완성되어 가면 추후에 아키텍처를 수정할 생각이다.

 

Jenkins는 도커를 사용해 환경을 구성하는게 편해 해당 방식을 사용했고

이후에 도커 이미지 빌드와 푸쉬를 위해 Docker를 사용해야 하지만

Jenkins 컨테이너에 Docker를 또 설치하는 것은 비효율적이라

호스트의 도커 소켓을 컨테이너와 공유하여 사용하게 컨테이너를 띄웠다.

 

배포 서버는 도커만 설치하여 Jenkins에서 CD 작업을 수행할 때

새로운 이미지로 컨테이너를 띄울 수 있게 구성했다.

 

 

Jenkins Pipeline

프리스타일 프로젝트를 사용해서 간단하게 만들 수도 있지만 Pipeline 방식이 뭔가 더 끌려서

해당 방식을 사용하게 되었는데, 이전에 Github Actions를 사용해봐서 그런지 생각보다 익숙했다.

 

Github보다 환경 변수나 인증 정보들을 관리하기도 더 편리하고, 아직 많은 플러그인을 사용해보진 못했지만

도커 이미지를 관리하거나 배포 서버의 SSH에 접근하는 것이 좀 더 간결하게 가능한거 같다.

 

 

Webhook

사실 개인적으로 가장 마음에 들었던 것은 이 웹훅인데 레포지토리에서 특정 동작이 수행될 때

Jenkins 서버에 웹훅을 날리게 되고 이때 받는 정보들을 사용해 하나의 파이프라인으로

여러 상황에서 공통적으로 사용할 수 있다는게 Github Actions와 비교 했을 때 좋다 생각한다.

 

Github Actions는 작성한 워크플로우가 해당 레포지토리에만 적용이 되는데

Jenkins는 웹훅을 통해 얻는 정보로 브랜치나 레포지토리 단위로 재사용을 할 수 있다.

 

 

사용한 Pipeline 예제

pipeline {
    environment {
        DOCKERHUB_CREDENTIALS = credentials('docker_hub')
        AWS_URL = '배포 서버의 EC2 IP'
    }
    
    agent any

    stages {        
        stage('Checkout') {
            steps {
                git branch: 'main', 
                    credentialsId: 'git_id', 
                    url: '깃허브 레포지토리 주소'
            }
        }
        
        stage('Test') {
			steps {
				dir('디렉토리가 다른 경우') {
					sh "chmod +x ./gradlew"
					sh "./gradlew clean test"
				}
			}
		}
        
        stage('Build') {
			steps {
				dir('디렉토리가 다른 경우') {
					sh "chmod +x ./gradlew"
					sh "./gradlew bootJar"
				}
			}
		}
		
		stage('JUnit') {
			steps {
				dir('디렉토리가 다른 경우') {
					junit '**/build/test-results/test/*.xml'
				}
			}
		}
		
		stage('Docker Build and Push') {
			steps {
				dir('kko-kko-noodles') {
				    sh 'docker login -u $DOCKERHUB_CREDENTIALS_USR -p $DOCKERHUB_CREDENTIALS_PSW'
					sh 'docker build -t 이미지명:$BUILD_NUMBER .'
					sh 'docker push 이미지명:$BUILD_NUMBER'
					sh "docker rmi 이미지명:$BUILD_NUMBER"
				}
			}
		}
		
		stage('Run'){
            steps{
                blueDeploy(AWS_URL)
            }
        }
    }
}

def blueDeploy(awsUrl){
    sshagent(['ec2_blue']) {
        sh """
            ssh -o StrictHostKeyChecking=no ubuntu@${awsUrl} '
                docker login -u $DOCKERHUB_CREDENTIALS_USR -p $DOCKERHUB_CREDENTIALS_PSW
                docker stop \$(docker ps -a -q)
        		docker rm \$(docker ps -a -q)
                docker rmi \$(docker images -q)
        	    docker pull 이미지명:$BUILD_NUMBER
        		docker run -d -p 80:8080 --name 컨테이너명 이미지명:$BUILD_NUMBER
    		'
		"""
    }
}

 

 

마치며

 

 

컨테이너를 잘못 만들어서 중간에 날리고 다시 만든 후에 19번 만에 마칠 수 있었는데

왜 벌써 해가 뜰 시간이 다가오는 것인지...

'Back-End' 카테고리의 다른 글

HTTP  (0) 2023.05.23
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 어노테이션을 필드에 추가해

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

 

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

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

+ Recent posts