728x90

정의

N + 1 보다는 1 + N이라고 볼 수 있는데

한 개의 쿼리에 예상과는 다른 N개의 추가 쿼리가 발생하는 문제다.

 

원인

즉시로딩, 지연로딩, 연관 관계와 상관 없이 언제든 N + 1 문제가 발생할 수 있다

 

즉시로딩의 경우

N:M 관계에서 N을 조회하는 쿼리를 날렸을 때

M을 즉시로딩으로 가져오게 되어있다면

N을 조회할 때 M을 한 번의 쿼리만 날려서 조회할거라 예상할 수 있지만

JPQL은 N을 먼저 조회하는 쿼리를 날린 후에

M을 즉시로딩으로 가져오게 설정된걸 확인하고

M을 가져오는 추가 쿼리를 날리게 된다.

 

일대일 관계라면 쿼리가 한 번이 아닌 2번 발생할 것이고

일대다 관계라면 1 + M개만큼의 쿼리가 발생한다.

 

지연로딩의 경우

N:M 관계에서 N을 조회한 후에

지연로딩으로 가져온 M의 객체들을 사용하려는 경우에는

실제 객체가 아닌 프록시 객체가 존재하는 상태이기 때문에

실제 객체를 조회하기 위해 쿼리문이 M번 더 발생하게 된다.

 

 

해결 방법

확실한 정답은 없지만 가장 간단한 해결 방법은 패치 조인을 사용하는 것이다.

 

기본 패치 타입이 즉시로딩인 경우에는 모두 패치 타입을 지연로딩으로 바꾼 후에

연관된 엔티티를 사용해야하는 경우에는 애초에 패치 조인으로 한 번에 가져오는 것이다.

 

물론 이 방법도 문제점이 존재하는데

대표적인 경우가 페이징 처리시 발생하는 문제다.

 

예를 들어 1페이지 당 10개씩 끊어서 페이징을 처리한다 했을 때

100만개의 데이터가 있다면 이 데이터를 모두 메모리에 가져온 후에

페이징 처리를 하게 되기 때문에 데이터의 양이 적다면 메모리 초과가 발생하진 않겠지만

데이터가 많다면 무조건 메모리 초과가 발생할 것이다.

 

일대일, 다대일 관계라면 몇 개의 데이터를 조회할지 측정이 가능하지만

일대다, 다대다 관계라면 정확한 측정이 어렵다.

 

이런 문제를 해결하기 위해서는 배치 사이즈를 조절하는 방법이 있는데

배치는 지연 로딩을 생각하면 이해하기 쉽다.

 

배치 사이즈가 적용되면 같은 쿼리

 

예를 들어, 배치 사이즈가 100이라면 100만 개의 데이터 중에서 100개의 베치를 가져온 후에

이후의 배치가 필요한 경우에는 IN 쿼리를 한 번 더 날리게 된다.

 

사실 함정이 한 가지 있는데 하이버네이트가 최적화를 해두었기 때문에

배치 사이즈를 100으로 설정했다고 100개씩 끊어서 쿼리를 날리진 않는다.

100
100/2 = 50
50/2 = 25
25/2 = 12
1 ~ 10

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 25, 50, 100]

 

배치 사이즈가 100인 경우에는 위와 같이 미리 여러 개의 prepared statement를 만들어 두는데

기본값인 legacy 전략은 데이터보다 적은 수를, padded 전략은 많은 수를 쿼리로 가져온다.

 

예를 들어, 조회하려는 데이터가 총 32개라면 in(?*25)와 in(?*7) 두 개의 쿼리가 날라가고

padded 전략이라면 in(?*50) 한 개의 쿼리가 날라간다.

 

이렇게 최적화를 해주는 이유는 배치 사이즈가 100이라고 in절을 in(?*1) 부터 in(?*100)까지

다 만들어두는 것은 성능적으로 비효율적이기 때문이다.  

 

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

스프링 면접 질문 정리 : 오브젝트와 의존관계  (0) 2024.04.03
[로깅] MDC를 이용한 식별된 로깅 처리  (1) 2024.03.05
슬라이스 테스트  (0) 2023.07.05
예외  (0) 2023.06.15
템플릿 정리  (0) 2023.06.14
728x90

슬라이스 테스트

슬라이스라는 단어처럼 자기가 테스트하고자 하는 계층 외에

나머지 계층들과는 단절하여 오로지 하나의 계층만 테스트하는 방법이다.

 

컨트롤러 슬라이스 테스트

서비스 영역 같은 다른 영역을 사용하지 않고 컨트롤러만 사용하여

테스트를 하고자 하는 경우에는 기본적인 양식은 아래와 같다.

@WebMvcTest(AnswerController.class)
public class QuestionControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private Gson gson;

    @MockBean
    private QuestionService questionService;

    @MockBean
    private QuestionMapper questionMapper;
    
    // 테스트 코드들
}

@WebMvcTest(테스트하려는 컨트롤러의 클래스)

해당 어노테이션을 사용하면 컨트롤러 테스트에 필요한 빈만 사용하여 테스트를 수행한다.

@SpringBootTest 어노테이션은 등록된 모든 빈을 사용하여 테스트를 수행하기 때문에

속도적인 측면에서 슬라이스 테스트에 사용하긴 효율적이지 못하다.

 

MockMvc

컨트롤러를 테스트하기 위해 API를 호출하고 검증하는 기능을 제공한다.

 

Gson

객체를 json 형식으로 바꿀 때 사용한다. (json 형식 이외에도 다른 형식으로도 바꿀 수 있다.)

 

@MockBean

테스트에 사용할 목 객체(가짜 객체)를 빈으로 등록하여 실제 빈 대신에 동작하게 한다.

스프링 테스트 프레임워크와 함께 쓰이며 통합 테스트에서 스프링 빈을 목 객체로 대체하려 사용한다.

 

@MockBean과 @Mock

두 어노테이션 모두 테스트 과정에 사용 할 목객체를 생성하고 대체하는 것은 똑같지만

어떤 프레임워크와 같이 사용되고 어느 테스트에 사용되는지가 다르다.

 

@MockBean 어노테이션은 위에서 언급했듯이 스프링 테스트 프레임워크와 함께 사용하여

주로 통합 테스트에 사용된다.

 

@Mock 어노테이션은 Mockito 프레임워크와 함께 사용되어 주로 단위 테스트에 사용된다.

 

컨트롤러 테스트에서는 스프링에서 제공하는 WebMvcTest를 사용하기 때문에

@MockBean 어노테이션을 사용한다.

 

@Test
public void postQuestionTest() throws Exception {
    long memberId = 1L;
    Member member = new Member();
    member.setMemberId(memberId);

    Question question = new Question();
    question.setQuestionTitle("Question Title");
    question.setQuestionText("Question Text");
    question.setQuestionStatus(Question.QuestionStatus.QUESTION_REGISTERED);
    question.setQuestionOpen(Question.QuestionOpen.SECRET);
    question.setMember(member);

    QuestionDto.Post postDto = new QuestionDto.Post(
            "Question Title",
            "Question Text",
            Question.QuestionOpen.PUBLIC
    );

    String requestContent = gson.toJson(postDto);

    given(questionMapper.questionPostToQuestion(Mockito.any(QuestionDto.Post.class)))
    .willReturn(new Question());
    
    given(questionService.createQuestion(Mockito.any(Question.class), Mockito.anyLong()))
    .willReturn(question);

    mockMvc.perform(
            post(root + "/create")
                    .param("memberId", String.valueOf(memberId))
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(requestContent)
    )
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.questionTitle").value(question.getQuestionTitle()));
}

POST 컨트롤러의 간단한 테스트 코드를 나눠서 살펴보겠다.

long memberId = 1L;
Member member = new Member();
member.setMemberId(memberId);

Question question = new Question();
question.setQuestionTitle("Question Title");
question.setQuestionText("Question Text");
question.setQuestionStatus(Question.QuestionStatus.QUESTION_REGISTERED);
question.setQuestionOpen(Question.QuestionOpen.SECRET);
question.setMember(member);

given에 사용될 데이터를 미리 하나 만들어둔다.

given 부분을 설명할 때 다시 설명하겠다.

QuestionDto.Post postDto = new QuestionDto.Post(
                "Question Title",
                "Question Text",
                Question.QuestionOpen.PUBLIC
        );

String requestContent = gson.toJson(postDto);

실제 클라이언트가 요청에 사용할 DTO 객체를 하나 만든 후에

해당 객체를 Gson을 사용해 json 형태로 변환한다.

given(questionMapper.questionPostToQuestion(Mockito.any(QuestionDto.Post.class)))
.willReturn(new Question());

QuestionMapper와 QuestionService 객체들은 컨트롤러만 슬라이스 테스트 하기 위해서는

불필요한 객체들이기 때문에 어떤 값을 전달 받아서 실행이 되어도

항상 정해진 결과를 반환하게 고정한다.

 

문법이 어렵게 느껴질 수도 있지만 코드를 읽어 보면 어렵지 않다.

 

QuestionMapper 객체의 questionPostToQuestion 메서드에

어떤 QuestionDto.Post 타입의 객체가 전달되어도

항상 Question 객체를 반환하겠다는 것이다.

given(questionService.createQuestion(Mockito.any(Question.class), Mockito.anyLong()))
.willReturn(question);

첫 given에서는 직접 정의해둔 결과가 아닌 임의의 새로운 질문 객체를 리턴하도록 지정했지만

마지막 given에서는 직접 정의해둔 객체를 리턴하도록 지정했다.

 

해당 컨트롤러에서는 QuestionService 객체의 createQuestion 메서드를 호출한

결과값이 리턴값이기 때문에 결과를 직접 정의한 객체로 정해줬다.

 

컨트롤러의 테스트는 결국에 클라이언트의 요청과 서버의 응답만 테스트하는 것이

목적이기 때문에 응답에 영향을 미치지 않는 중간 과정들은 어떤 값을 리턴하든 상관 없고

최종적인 결과에 영향을 미치는 과정만 리턴 값을 정의해주면 된다.

mockMvc.perform(
        post(root + "/create")
                .param("memberId", String.valueOf(memberId))
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestContent)
)
        .andExpect(status().isCreated())
        .andExpect(jsonPath("$.questionTitle").value(question.getQuestionTitle()));

지금까지는 테스트를 수행하기 위한 준비 과정이었다면

이제 MockMvc의 perform 메서드를 사용해 컨트롤러 테스트를 할 수 있다.

post(root + "/create")

위의 코드처럼 어떤 종류의 요청을 테스트할지 정한 후에

해당 요청을 받을 주소를 설정해준다.

.param("memberId", String.valueOf(memberId))
.contentType(MediaType.APPLICATION_JSON)
.content(requestContent)

해당 요청에 대한 추가적인 정보들을 지정해주면 요청에 대한 응답 데이터를 담은

ResultActions 객체가 반환된다.

 

위의 코드에서는 주소의 파라미터 값(?memberId=1)을 param 메서드를 사용해 지정해주고

기존에 Gson을 사용해 json 형식으로 만들어두었던 요청 데이터를

클라이언트가 요청을 보낼 때 사용할 데이터로 지정해준다.

.andExpect(status().isCreated())
.andExpect(jsonPath("$.questionTitle").value(question.getQuestionTitle()));

ResultActions 객체가 가지고 있는 데이터를 사용해

 

서비스 슬라이스 테스트

컨트롤러 슬라이스와 마찬가지로 서비스도 서비스가 의존하고 있는 객체과의

관계를 끊고 목 객체를 이용하여 테스트하면 된다.

@Service
public class MemberService {
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

위와 같은 구조의 서비스 클래스를 테스트 해보자

@ExtendWith(MockitoExtension.class)
public class MemberServiceTest {
    @Mock
    private MemberRepository memberRepository;

    @InjectMocks
    private MemberService memberService;
    
    // 테스트 코드 생략
}

서비스 슬라이스 테스트는 기본적으로 위와 같은 양식을 가진다.

 

@ExtendWith(추가할 확장 클래스)

JUnit5의 확장 모델을 사용하기 위해 사용하는 어노테이션이다.

현재 테스트 클래스에서는 Mockito 확장을 추가하기 위해 사용하였다.

 

@Mock

컨트롤러 테스트에서 알아봤듯이 Mockito 프레임워크를 사용할 때

목 객체를 생성하기 위한 어노테이션이다.

 

@InjectMocks

@Mock 어노테이션이 적용된 객체들을 해당 어노테이션이 적용된 객체에 주입한다.

 

서비스 클래스에서 의존관계를 주입하는 것과 같은 작업이라고 볼 수 있다.

@Test
public void saveMemberTest() throws Exception{
    Member member = new Member();
    member.setMemberName("이름");
    member.setMemberId(1L);
    member.setMemberEmail("test@email.com");

    given(memberRepository.save(Mockito.any(Member.class))).willReturn(member);

    assertThat(
            memberService.createMember(member).getMemberName(),
            is(equalTo(member.getMemberName()))
    );
}

테스트 하는 방식은 컨트롤러와 다를게 없다.

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

[로깅] MDC를 이용한 식별된 로깅 처리  (1) 2024.03.05
[JPA] N + 1 문제와 해결 방법  (1) 2024.03.01
예외  (0) 2023.06.15
템플릿 정리  (0) 2023.06.14
JDBC Template  (0) 2023.06.13
728x90

예외처리 시 하지 말아야 할 행동

모든 예외는 반드시 적절하게 복구하거나 중단시키고 개발자에게 통보되어야 하는데

예외를 잡고 어떠한 처리도 하지 않고 넘어가거나 출력만 하는 행동과

무의미하고 무책임하게 다른 곳으로 예외를 던져버리는 행동은 

무슨 일이 있어도 해서는 안되는 행동이다.

 

예외의 종류

CheckedException

말그대로 예외를 확인하고 처리해야 하는 경우를 말하며

일반적으로 예외라고 하면 이 ChekedException을 말한다.

 

이때 예외처리는 필수이기 때문에

처리해주지 않을 경우 컴파일을 에러가 발생한다.

 

대표적으로 IOException, SQLException 등이 있다.

UncheckedException

RuntimeException 클래스를 상속 받은 예외들로

ChekedException처럼 예외처리가 필수는 아니다.

 

컴파일에는 문제 없지만 런타임 시에 문제가 생기는 경우로

개발자의 부주의로 인한 문제기 때문에 예외처리가 아니더라도

코드의 수정을 통해서 방지할 수 있다.

 

대표적으로 아무것도 할당되지 않은 빈 참조변수를 사용하는 경우

발생하는 NullPointerException이 있다.

Error

Error 클래스를 상속 받는 서브 클래스들을 뜻한다.

 

시스템적으로 비정상적인 오류가 발생한 경우로

보통 자바 가상머신에서 발생시키기 때문에 개발자가 코드로 잡을 수 있는 것이 아니다.

 

대표적으로 OutOfMemoryError가 있는데

개발자가 이러한 현상을 예방하기 위한 코드를 작성하는 것은 가능해도

이러한 에러가 생긴 경우를 처리하는 코드를 작성할 수는 없다.

 

그렇기 때문에 Error는 개발자가 직접 신경쓸 영역이 아니다.

Exception

Exception 클래스를 상속 받는 서브 클래스들로 에러와는 다르게

개발자가 작성한 코드에 문제가 있는 경우 발생한다.

 

이 말은 개발자가 직접 예외를 처리할 수 있다는 것인데,

Exception 클래스의 서브 클래스 중에서

RuntimeException 클래스의 상속 여부에 따라

상속 받지 않는 CheckedException과 상속 받는 UncheckedException이 있다.

Runtime Exception

RuntimeException 클래스를 상속 받은 서브 클래스들이 해당된다.

 

예외처리 방법

예외 복구

말그대로 예외가 어떻게 발생했는지 파악하고 해결하여 정상적으로 만드는 방법이다.

 

특정 횟수만큼 반복해서 재시도해보거나 다른 방법으로 전환하여 시도하는 등의

방법을 통하여 복구 시도를 하는데 어떤 식으로든 예외를 복구할 가능성이

있는 경우에만 해당 방법을 사용한다.

예외처리 회피

예외를 직접 처리하는 것이 아닌 호출한 쪽으로 떠넘기는(던지는) 방법이다.

 

예외를 던질 때는 받는 곳에서 해당 예외를 처리할 수 있어야 하는데

예외를 처리하지도 못하는 곳에 무작정 던지고 보는 것은

무책임한 회피 방식이기에 좋지 않다.

 

객체지향에서 하나의 객체는 관련된 기능만 수행하게 설계되니

만약 DAO에서 발생하는 예외는 꼭 그 안에서 해결해야 하며,

전혀 상관 없는 서비스나 컨트롤러에 예외를 넘기는 것은 하지 말아야 한다.

 

예외를 회피할 때는 반드시 의도가 분명한 경우에만 해야한다.

예외 전환

예외처리 회피처럼 예외를 떠넘기는 것은 똑같지만

예외를 바꿔서 던져준다는 것이 다르다.

 

이 방법은 보통 두 가지 상황에서 사용한다.

try {
	// 예외가 발생할 가능성이 있는 코드	
}
catch (SQLException s) {
	// 에러 코드가 특정 에러와 같은 경우
    // 특정 에러의 예외로 바꿔서 던져줌
	if(s.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY) {
    	// 기존예외를 담아서 전달하는 중첩예외
    	throw DuplicateUserIdException(s);
    }
    // 그렇지 않은 경우 기존 예외를 다시 던짐
    else {
    	throw s;
    }
}

첫 번째로는 예외의 의미를 분명하게 해주는 경우에 사용하는데

IOException처럼 의미의 범위가 큰 예외를 던져주면

입출력에서 어떤 예외가 발생했는지 알기가 어렵다.

 

그래서 IOException 예외보다 좀 더 상세하고 확실한 의미를

가지고 있는 예외로 바꾸어 던져주는 방법이다.

 

또한 getCause 메서드를 이용하여 기존 예외를 알 수가 있어서

기존 예외를 담아서 중첩 예외로 던지는 것이 좋다.

try {
	OrderHome orderHome = EJBHomeFactory.getInstance().getOrderHome();
    Order order = orderHome.findByPrimaryKey(Integer id);
} catch (NamingException ne) {
	throw new EJBException(ne);
} catch (SQLException se) {
	throw new EJBException(ne);
}

두 번째로는 예외를 처리하기 쉽게 포장하는 것이다.

 

중첩 예외를 만드는 것은 똑같지만

의미를 명확한 예외로 전환하는 것이 아닌

강제로 처리해야하는 체크 예외를 언체크 예외로 바꿀 때 사용한다.

 

의미 있는 예외이거나 복구 가능한 예외가 아닌 경우

다루기 쉬운 런타임 예외로 바꾸는 것이다.

 

예외처리 전략

런타임 예외의 보편화

보통 체크 예외는 일반적인 예외, 언체크 예외는 시스템 장애나 프로그램상의 오류를 처리한다.

 

체크 예외는 복구 가능성이 조금이라도 있는 예외적인 상황이기에

이 예외의 처리를 강제한다.

 

프로그램의 경우에는 잘못된 요청을 받아서 예외가 생겼다고

종료를 해버릴 수 없기에 예외를 강제로 처리해야했지만,

서버에서는 수많은 요청 중에서 예외가 생긴 요청만 중단시키면 된다.

 

그래서 서버에서는 체크 예외의 활용도가 낮기 때문에

체크 예외를 직접적으로 다루기보단 런타임 예외로 전환해서

처리하는 것이 좋을 수도 있다.

애플리케이션 예외

의도적으로 발생시키는 예외로, 반드시 조치를 취하도록 요구하는 예외다.

 

은행 프로그램에서 돈을 꺼내는 경우에 잔고 부족 같은 상황은

언체크 예외로 그냥 넘어가고 돈을 꺼내면 안되는 상황이기에

체크 예외를 만들어서 의도적으로 예외를 발생시키는 경우가 이에 해당한다.

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

[JPA] N + 1 문제와 해결 방법  (1) 2024.03.01
슬라이스 테스트  (0) 2023.07.05
템플릿 정리  (0) 2023.06.14
JDBC Template  (0) 2023.06.13
템플릿과 콜백  (0) 2023.06.13
728x90
  1. 데이터베이스 로직 같은 예외 발생 가능성이 높거나 공유 리소스의 반환이 필요한 코드는 예외 처리 블록으로 별도로 관리해줘야 함
  2. 일정하게 반복되는 작업 흐름에서 일부만 다른 코드가 존재하는 경우 전략 패턴을 사용하는 것이 좋고, 바뀌지 않는 부분을 컨텍스트, 바뀌는 부분을 전략으로 만들고 이 두 오브젝트를 인터페이스로 유연하게 연결한다.
  3. 클라이언트 메서드를 통해 직접 전략을 정의하고 제공하여 사용하게 한다.
  4. 클라이언트 메서드 안에 익명 내부 클래스를 사용하여 전략을 구현하면 클라이언트 메서드의 정보를 사용할 수도 있고 코드를 간결하게 작성할 수 있다.
  5. 컨텍스트가 여러 클라이언트에서 사용된다면 별도의 클래스로 분리하여 공유되게 만든다.
  6. 컨텍스트는 별도의 빈으로 등록하여 의존관계를 주입 받거나 클라이언트에서 직접 생성해서 사용한다.
  7. 템플릿/콜백 패턴은 클라이언트로부터 생성된 콜백 객체를 전달 받아 일정하고 반복적인 로직을 수행하는 템플릿이 상황에 따른 로직을 수행하는 콜백 메서드에 전달할 정보를 생성하여 전달한 후에 콜백 메서드가 전달 받은 정보로 특정 로직을 수행한 후 결과를 리턴하여 템플릿이 최종 결과를 클라이언트에 리턴하는 패턴이다.
  8. 콜백의 코드에도 일정한 패턴이 반복되면 콜백을 템플릿에 넣고 재활용하는 것이 좋다.
  9. 템플릿과 콜백의 타입이 다양하게 바뀔 수 있다면 제네릭스를 이용한다. Collback<T>
  10. 스프링에서는 JDBC를 위해 JdbcTemplate 기반의 템플릿과 콜백을 제공한다.
  11. 템플릿은 한 번에 여러개의 콜백을 사용하거나, 하나의 콜백을 여러 번 호출 가능하다.
  12. 템플릿/콜백 패턴을 설계할 때는 사이에 주고 받는 정보를 중요하게 봐야 함.

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

슬라이스 테스트  (0) 2023.07.05
예외  (0) 2023.06.15
JDBC Template  (0) 2023.06.13
템플릿과 콜백  (0) 2023.06.13
컨텍스트와 DI  (0) 2023.06.13
728x90

스프링이 제공하는 JDBC를 이용하는 DAO에서 사용할 수 있는 템플릿과 콜백

 

private JdbcTemplate jdbcTemplate;

public void setDataSource(DataSource dataSource) {
    this.jdbcTemplate = new JdbcTemplate(dataSource);

    this.dataSource = dataSource;
}

JDBC Template을 사용하기 위해 기존의 JdbcContext를 빼고 바꿔준다.

update

기존 전략 인터페이스의 메서드를 통해 전략을 적용했던 것과 똑같이

JDBC Template이 제공하는 콜백 중 PreparedStatementCreator 인터페이스의

createPreparedStatement 메서드를 사용하면 된다.

 

PreparedStatementCreator 타입의 콜백을 받아서 사용하는

JDBC Template의 메서드는 update()다

 

기존의 코드와 비교해보겠다.

public void deleteAll() throws SQLException {
    workWithStatementStrategy(
        new StatementStrategy() {
            public PreparedStatement makePreparedStatement(Connection c)
                    throws SQLException {
                return c.prepareStatement(executeSql());
            }
        }
    );
}
public void deleteAll() throws SQLException {
    this.jdbcTemplate.update("delete from users");
}

위가 기존의 코드고 아래가 JDBC 템플릿의 콜백 메서드를 사용한 예시다.

 

눈으로만 봐도 코드가 간결해진 것을 알 수 있는데

executeSql 메서드를 사용하여 쿼리문을 전달 받던거만 제외하면

코드의 구성은 똑같다고 볼 수 있다.

public void add(final User user) throws SQLException {
    this.jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)",
                    user.getId(), user.getName(), user.getPassword());
}

add 작업도 마찬가지로 update 메서드를 사용하여 할 수 있다.

추가하고자 하는 양식에 맞게 순서대로 파라미터로 전달해주면 된다.

queryForInt

public int getCount() throws SQLException  {
    Connection c = dataSource.getConnection();

    PreparedStatement ps = c.prepareStatement("select count(*) from users");

    ResultSet rs = ps.executeQuery();
    rs.next();
    int count = rs.getInt(1);

    rs.close();
    ps.close();
    c.close();

    return count;
}

위의 메서드에서는 쿼리를 실행한 후에 결과를 ResultSet을 통해 값을 가져온다.

해당 작업에서도 사용할 수 있는 템플릿이 있다.

 

마찬가지로 PreparedStatementCreator 콜백과 ResultSetExtractor 콜백을

파라미터로 받는 query 메서드를 사용한다.

 

ResultSetExtractor 콜백은 PreparedStatement의 쿼리를 실행해 얻은

ResultSet을 전달 받는 콜백이다.

public int getCount() {
	return this.jdbcTemplate.query(new PreparedStatementCreator() {
    	public PreparedStatement createPreparedStatement(Connection c) throws SQLException {
        	return c.preparedStatement("select count(*) from users");
        }
    }, new ResultSetExtractor<Integer>() {
    	public Integer extractorData(ResultSet rs) throws SQLException {
        	rs.next();
            return rs.getInt(1);
        }
    }
}

위의 코드는 query 메서드를 적용한 코드다.

 

살펴보면 query 메서드의 파라미터로

PreparedStatementCreator 콜백과 ResultSetExtractor 콜백을 전달 받는다.

 

PreparedStatementCreator 콜백은 템플릿으로부터

커넥션을 받아 PreparedStatement(SQL문)을 돌려준다.

 

ResultSetExtractor 콜백은 템플릿으로부터

ResultSet을 받아 추출한 결과를 돌려준다.

 

즉, query(쿼리문, ResultSet에서 추출한 결과)

 

위의 query 메서드에서 사용되는 콜백 오브젝트들은 모두 재사용하기 좋은 구조를 가지고 있는데,

PreparedStatementCreator 콜백은 재사용 방법을 알아봤으니

ResultSetExtractor 콜백의 재사용 방법을 알아보겠다.

public int getCount() {
    return this.jdbcTemplate.queryForInt("select count(*) from users");
}

위의 코드처럼 JDBC 템플릿에서 제공하는 queryForInt 메서드를 사용하면

위의 긴 코드를 한 줄로 간단하게 사용할 수도 있다.

 

JDBC 템플릿은 스프링이 제공하는 클래스이지만 DI 컨테이너를 필요로 하지 않아서

사용하고자 하는 클래스에서 JdbcTemplate 오브젝트를 생성하고

필요한 DataSource를 전달해주기만 하면 된다.

queryForObject

public User get(String id) throws SQLException {
    Connection c = this.dataSource.getConnection();
    PreparedStatement ps = c
            .prepareStatement("select * from users where id = ?");
    ps.setString(1, id);

    ResultSet rs = ps.executeQuery();

    User user = null;
    if (rs.next()) {
        user = new User();
        user.setId(rs.getString("id"));
        user.setName(rs.getString("name"));
        user.setPassword(rs.getString("password"));
    }

    rs.close();
    ps.close();
    c.close();

    if (user == null) throw new EmptyResultDataAccessException(1);

    return user;
}

위의 코드는 기존의 get 메서드인데 코드가 아주 길고 복잡하다.

 

PreparedStatementCreator 콜백을 사용하여 SQL문도 처리해야하고

SQL문이 바인딩이 필요한 치환자(id)를 갖고 있으며,

쿼리 결과를 User 오브젝트로 만들어야 한다.

 

getCount 메서드처럼 특정 값을 리턴하는 것이 아닌

오브젝트를 리턴해야하기 때문에 ResultSetExtractor 콜백을 사용하는 것이 아니라

RowMapper 콜백을 사용해야 한다.

 

두 개의 콜백 모두 템플릿으로부터 ResultSet을 전달 받아 필요한 정보를 추출하지만

ResultSetExtractor은 한 번만 전달 받아 알아서 결과를 리턴해주지만

RowMapper은 로우 하나를 매핑할 때마다 호출된다.

 

get 메서드는 결국 사용자 정보를 하나 가져오는 메서드이기 때문에

하나의 사용자 정보만 매핑하면 되니 ResultSet의 첫 번째 로우만 매핑한다.

 

이러한 작업들을 해주는 템플릿 메서드는 queryForObject가 있다.

queryForObject(쿼리문, 조회할 조건(바인딩할 파라미터 값), RowMapper 콜백으로 매핑)

위와 같은 양식으로 코드를 작성하면 된다.

public User get(String id) {
    return this.jdbcTemplate.queryForObject("select * from users where id = ?",
            new Object[] {id}, 
            new RowMapper<User>() {
                public User mapRow(ResultSet rs, int rowNum)
                        throws SQLException {
                    User user = new User();
                    user.setId(rs.getString("id"));
                    user.setName(rs.getString("name"));
                    user.setPassword(rs.getString("password"));
                    return user;
                }
            });
}

코드가 엄청 혁신적으로 짧아지고 그런 것은 아니지만 어느정도 간결해졌다.

 

queryForObject 템플릿 메서드의 첫 번째 파라미터로

"select * from users where id = ?" 라는 쿼리문을 전달했고

 

두 번째 파라미터로 해당 쿼리문에 바인딩 하기 위한 기본키인 id를 전달 후에

쿼리문의 결과를 RowMapper 콜백을 통해 User 오브젝트로 매핑한다.

 

query()

queryForObject 템플릿 메서드는 하나의 사용자만 가져오는 경우에 사용했다면,

모든 사용자의 정보를 가져오는 기능에 사용하는 템플릿 메서드도 있다.

 

기존에는 한 명의 사용자 정보만 가져왔으나 이번엔 모든 사용자를 가져오니

User 오브젝트 타입의 List를 사용하면 된다.

public List<User> getAll() { }

모든 사용자의 정보를 가져오는 메서드의 시그니처는 위와 같이 선언한다.

 

queryForObject와 비슷하지만 모든 값을 가져오는 메서드이니

파라미터로 바인딩할 조건을 받을 필요가 없이

쿼리문과 RowMapper 콜백만 받는다.

 

조건이 있는 경우에는 바인딩할 파라미터를 추가한다.

return this.jdbcTemplate.query("select * from users order by id",
				new RowMapper<User>() {
					public User mapRow(ResultSet rs, int rowNum)
							throws SQLException {
						User user = new User();
						user.setId(rs.getString("id"));
						user.setName(rs.getString("name"));
						user.setPassword(rs.getString("password"));
						return user;
					}
				});

모든 사용자 정보를 id순으로 얻는 쿼리문의 수행 결과인 ResultSet의

모든 로우를 열람하면서 로우마다 RowMapper 콜백을 호출하여

User 오브젝트로 변환하여 리스트에 추가한다.

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

예외  (0) 2023.06.15
템플릿 정리  (0) 2023.06.14
템플릿과 콜백  (0) 2023.06.13
컨텍스트와 DI  (0) 2023.06.13
전략 패턴 최적화 하기  (0) 2023.06.13
728x90

전략 패턴은 복잡하지만 바뀌지 않는 일정한 패턴을 갖는 작업 흐름이 존재하고

그중 일부분만 자주 바꿔 사용하는 경우 적합한 구조다.

 

전략 패턴의 기본 구조에 익명 내부 클래스를 활용한 방식

템플릿/콜백 패턴이라고 한다.

 

컨텍스트는 템플릿에 해당하고 익명 내부 클래스로 만들어지는 오브젝트를 콜백에 해당한다.

템플릿

어떤 목적을 위해 미리 만들어둔 모양이 있는 틀로 고정된 틀 안에서

특정 부분만 상황에 맞게 바꿔 사용하는 경우를 말한다.

JdbcContext처럼 공통적인 기능에서 특정 부분만 전략에 맞게 사용하는 것과 같다.

콜백

특정 로직을 담은 메서드를 실행시키기 위해 다른 오브젝트의 메서드에 전달되는 오브젝트

JdbcContext에 전달되어 실행되는 전략과 같다.

템플릿/콜백의 동작 원리

  1. 클라이언트(CRUD 메서드)가 콜백(익명 내부 클래스)을 생성
  2. 클라이언트가 템플릿(컨텍스트)에 콜백을 전달하며 호출
  3. 템플릿의 로직 시작 및 참조정보(JDBC 정보) 생성
  4. 템플릿이 콜백을 호출하며 참조정보를 전달
  5. 클라이언트 및 전달 받은 참조정보 등을 통해 로직 수행
  6. 템플릿에 로직 결과를 리턴
  7. 템플릿의 남은 로직 수행 및 마무리
  8. 클라이언트에 템플릿의 작업 결과를 리턴

콜백의 분리와 재활용

클라이언트의 메서드가 간결해지고 최소한의 로직만 갖고 있게 된다는 장점이 있지만

익명 내부 클래스를 통해 코드를 작성하는 것은 익숙한 방식이 아니기도해서

코드의 작성이나 가독성이 떨어진다는 단점이 있다.

 

기존에 했던 작업들처럼 익명 내부 클래스를 통해 오브젝트를 만드는 코드에서

반복적으로 수행되는 작업을 분리하면 이러한 문제를 줄일 수 있다.

new StatementStrategy() {			
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
      return c.prepareStatement("변하는 SQL문");
    }
}

위의 코드에서 SQL문을 리턴해주는 부분만 빼면 콜백 클래스의

정의와 생성은 어느 메서드에서 사용하든 일치한다는 것을 알 수 있다.

 

반복되는 부분인 콜백 클래스의 정의와 생성을 메서드로 분리하고

분리한 메서드의 파라미터로 변하는 부분인 SQL문을 전달하면 된다.

private void executeSql(final String query) throws SQLException {
	this.jdbcContext.workWithStatementStartegy(
    	new StatementStrategy() {			
            public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
				return c.prepareStatement("변하는 SQL문");
            }
        }
    );
}

이렇게 분리된 메서드를 add와 delete 등의 DAO의 메서드에서 그대로 재사용하면 된다.

public void deleteAll() throws SQLException {
	executeSql("delete from users");
}

당연히 코드가 훨씬 간결해졌다.

콜백과 템플릿 결합하기

마찬가지로 executeSql 메서드도 모든 DAO 클래스에서 사용해야하는

공통적인 기능이기 때문에 공유하는 것이 좋다.

 

해당 메서드를 공유해서 사용할 수 있게 JdbcContext 클래스로 옮겨주기만 하면된다.

public class JdbcContext {
	public void executeSql(final String query) throws SQLException {
        workWithStatementStartegy(
            new StatementStrategy() {			
                public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                    return c.prepareStatement("변하는 SQL문");
                }
            }
        );
    }
}

성격이 다른 코드이기 때문에 분리하는 것이 좋을 수도 있지만

하나의 목적을 위해 서로 연결되어 있는 응집력이 강한 코드이기 때문에

한 곳에 모아두는 것이 오히려 유리하다.

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

템플릿 정리  (0) 2023.06.14
JDBC Template  (0) 2023.06.13
컨텍스트와 DI  (0) 2023.06.13
전략 패턴 최적화 하기  (0) 2023.06.13
템플릿 적용하기  (0) 2023.06.13
728x90

전략 패턴의 구조에서는

클라이언트는 add, delete 같은 메서드에 해당하고

전략은 익명 내부 클래스에 해당하며

jdbcContextWithStatementStrategy 메서드를 컨텍스트라고 볼 수 있다.

 

여기서 컨텍스트는 User의 DAO에서만 사용하는 것이 아니라

다른 DAO에서도 공통적으로 사용할 수 있는 기능에 해당하기 때문에

해당 컨텍스트를 모든 DAO에서 사용할 수 있게

별도의 클래스로 독립시키는 것이 좋다.

 

클래스 분리하기

별도의 클래스를 만들어 컨텍스트 메서드를 옮기고 DI를 알맞게 설정해준다.

public class JdbcContext {
	DataSource dataSource;
	
	public void setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;
	}
	
	public void workWithStatementStrategy(StatementStrategy stmt) throws SQLException {
		Connection c = null;
		PreparedStatement ps = null;

		try {
			c = dataSource.getConnection();

			ps = stmt.makePreparedStatement(c);
		
			ps.executeUpdate();
		} catch (SQLException e) {
			throw e;
		} finally {
			if (ps != null) { try { ps.close(); } catch (SQLException e) {} }
			if (c != null) { try {c.close(); } catch (SQLException e) {} }
		}
	}
}

DAO 클래스에서 해당 컨텍스트 클래스를 DI 받아서 사용할 수 있게 한다.

public class UserDao {
    private JdbcContext jdbcContext;
		
	public void setJdbcContext(JdbcContext jdbcContext) {
		this.jdbcContext = new JdbcContext();
	}
    // 생략
}

빈 의존관계 변경하기

스프링의 의존관계 주입은 기본적으로 인터페이스를 사이에 두고 의존 클래스를

상황에 맞게 유연하게 바꿔서 사용할 수 있게 하는 것이 목적이지만

JdbcContext 클래스 같이 바뀔 일이 없는 클래스는 굳이 인터페이스를 구현하여

의존관계를 주입해줄 필요는 없다.

인터페이스를 사용하지 않는 DI 적용

코드에 따라 문제가 될 수도 있고 안될 수도 있지만

JdbcContext 같은 경우는 문제가 되지 않는 정상적인 DI 방식을 적용했다고 볼 수 있다.

 

여러가지 이유가 있는데 첫 번째로는 

JdbcContext는 그저 컨텍스트 메서드를 제공하는 서비스 오브젝트로서의 의미만 있고

오브젝트 자체가 빈으로 등록되어 싱글톤으로 공유되어 사용되는 것이 이상적이다.

 

두 번째는 JdbcContext가 DI를 통해 DataSource라는 빈에 의존하고 있기 때문인데

스프링의 DI를 위해서는 주입되는 쪽과 주입받는 쪽이 모두 빈으로 등록되야 하기 때문이다.

코드를 이용하는 수동 DI

DAO 클래스 내부에서 직접 DI를 적용하는 방법인데, 이 방법을 사용하면

JdbcContext를 싱글톤으로 만드는 것은 불가능하다.

 

그렇다고 계속해서 JdbcContext 오브젝트를 생성하는 것은 아니고

DAO 오브젝트마다 하나의 JdbcContext 오브젝트를 공유하게 한다.

 

하지만 위에서 살펴봤듯이 컨테이너를 통해 DI를 받으려면

양쪽 모두 스프링 빈으로 등록이 되어있어야 하는데 이 방법을 사용하면

JdbcContext는 빈으로 등록이 되었지 않기 때문에 DataSource를 주입받을 수 없다.

 

이러한 문제를 해결하기 위해서는 DAO에 JdbcContext의 DI까지 맡기면 된다.

즉, JdbcContext아 DI 받아야 할 DataSource를 DAO가 대신 DI를 받는다.

public class UserDao {
	private DataSource dataSource;
		
    public void setDataSource(DataSource dataSource) {
        this.jdbcContext = new JdbcContext();
        this.jdbcContext.setDataSource(dataSource);

        this.dataSource = dataSource;
    }

    private JdbcContext jdbcContext;
}

이렇게 JdbcContext 대신 DataSource를 DI 받는다.

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

JDBC Template  (0) 2023.06.13
템플릿과 콜백  (0) 2023.06.13
전략 패턴 최적화 하기  (0) 2023.06.13
템플릿 적용하기  (0) 2023.06.13
스프링에 테스트 적용하기  (0) 2023.06.08
728x90

전략 패턴을 사용하여 메서드 추출을 했을 때보다 

유연하고 확장성 있는 코드를 짜는 것은 성공했지만

여전히 필요할 때마다 전략 패턴을 새로 생성해야 하는 것은 그대로다.

 

또한, 전략에 부가적인 정보가 필요한 경우

이를 위해 생성자와 인스턴스 변수를 추가적으로 만들어야 하는 것도 번거롭다.

 

로컬 클래스로 사용하기

전략 클래스를 계속해서 생성해야 하는 문제는 전략 클래스를

컨텍스트 클래스에 내부 클래스로 정의하는 것으로 해결할 수 있다.

 

이러한 전략 클래스들은 컨텍스트 클래스 이외에는 사용되지 않기 때문에

굳이 외부에 클래스로 따로 만들어둘 필요가 없기 때문이다.

public void add(User user) throws SQLException {
	// 사용할 컨텍스트 메소드 내부에 클래스 선언
	class AddStatement implements StatementStartegy {
    	// 전략 구현
    }
}

기존에 별도의 클래스에 작성되어 있던 코드를 위와 같이 해당 전략을 사용하고자 하는

컨텍스트 메서드 내부에 선언하면 클래스 파일을 추가하는 작업이 사라진다.

 

또한 해당 컨텍스트 메서드에서 사용되는 멤버들에 접근할 수 있다는 장점도 있기 때문에

생성자를 통해 필요한 오브젝트들을 일일히 생성하는 번거로움이 사라지고

컨텍스트 메서드의 파라미터나 멤버들에 접근해 사용하면 된다.

익명 내부 클래스로 더 간결하게 만들기

특정 컨텍스트 메서드에서만 사용될 전략 클래스이기 때문에

다른 곳에서 호출 할 필요가 없으니 클래스의 이름이 굳이 필요 없다.

public void add(User user) throws SQLException {
	jdbcContextWithStatementStrategy(
		StatementStrategy st = new StatementStrategy() { // 전략 구현부 }
    );
}

어차피 한 번만 사용할 오브젝트니 굳이 변수에 저장할 필요도 없다.

public void add(User user) throws SQLException {
	jdbcContextWithStatementStrategy(
		new StatementStrategy() { // 전략 구현부 }
    );
}

클래스 파일도 줄어들고 코드도 훨씬 간결해졌다.

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

템플릿과 콜백  (0) 2023.06.13
컨텍스트와 DI  (0) 2023.06.13
템플릿 적용하기  (0) 2023.06.13
스프링에 테스트 적용하기  (0) 2023.06.08
JUnit 자세히 알아보기  (0) 2023.06.08

+ Recent posts