Spring JDBC 연동 테스트

최민길(Gale)·2023년 1월 11일
1

Spring Boot 적용기

목록 보기
8/46

안녕하세요 오늘은 Spring JDBC를 연동해보면서 단위 테스트까지 진행하는 내용에 대해서 포스팅해보도록 하겠습니다.


출처 : https://velog.io/@seungho1216/Spring-BootController-Service-Repository%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC

저번 포스팅에서는 Controller를 구축하고 단위 테스트를 진행해보았습니다. 이번 시간에는 DB와 연동하기 위해 위의 이미지처럼 Repository, Service를 차례대로 구현해보겠습니다. 저번 시간에 진행한 Controller의 경우 클라이언트 측의 요청을 가장 먼저 받아 URI별로 처리할 로직의 방향을 설정하는 역할을 담당합니다. Repository의 경우 쿼리를 실행하여 원하는 데이터를 받아오는 역할을 담당합니다. Service의 경우 Controller와 Repository의 중간에서 정보 변동의 위험이 있는 로직을 처리하여 Controller로 전달합니다. 이 때 Service를 인터페이스로 ServiceImpl을 구현체로 인터페이스를 implement하여 ServiceImpl 내에서 독립적으로 기능을 확장할 수 있으며 Controller는 Service 인터페이스를 사용하기 때문에 내부가 변경되어도 영향을 받지 않는 구조로 사용하기도 합니다.

데이터 객체는 크게 DAO와 DTO로 나뉠 수 있습니다. DAO(Data Access Object)는 데이터베이스의 데이터에 직접 접근하는 객체이며, DTO(Data Transfer Object)는 계층 간 데이터 교환을 위해 사용되는 객체입니다. 따라서 DB와 직접 연동하는 부분은 DAO, 각 부분 별로 데이터를 전달할 때는 DTO 객체를 통해 전달하게 됩니다.

    // JDBC
    implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'

그럼 지금부터 JDBC를 이용한 DB 연동을 진행해보겠습니다. 우선 build.gradle 설정 파일에서 다음의 dependency를 적용해주세요.

spring.datasource.driverClassName=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://{DB IP}:3306/{사용할 테이블}
spring.datasource.username={DB 접속 계정}
spring.datasource.password={DB 접속 패스워드}

이어서 application.properties 파일에 다음의 정보를 기입해주세요. 이 부분에 DB 정보를 기입하지 않고 환경 변수로서 입력받아 코드의 보안성을 강화시키는 방법도 존재합니다. 하지만 테스트 환경에서는 이렇게 파일로 관리하고 추후 배포 진행 시 환경 변수로 따로 빼서 보안을 강화하는 방법도 좋을 것 같습니다.

package com.example.test.model;

public class UserDTO {
    public Long id;
    public String name;

    public UserDTO(Long id, String name){
        this.id = id;
        this.name = name;
    }

    // 테스트 시 비교를 위한 코드 추가
    @Override
    public int hashCode() {
        return Integer.parseInt(String.valueOf(this.id));
    }

    @Override
    public boolean equals(Object obj) {
        // 비교할 DTO 생성
        UserDTO userDTO = (UserDTO) obj;
        
        // 두 객체가 같은지 다른지 체크
        boolean status = false;
        
        // 내부의 모든 데이터가 같다면 true
        if(this.name.equalsIgnoreCase(userDTO.name)
                && this.id == userDTO.id){
            status = true;
        }
        
        // 그렇지 않다면 false
        return status;
    }
}

이번 실습에서는 User 정보를 가져오는 실습을 해보도록 하겠습니다. 가져올 User 정보를 UserDTO 클래스를 정의하여 추가합니다. 이번 실습의 경우 간단하게 코드를 구성하여 DAO가 DTO의 역할까지 같이 진행하기 때문에 편의상 DTO로 통일하여 네이밍을 지었습니다. 이 때 hashCode와 equals 메소드를 오버라이드하였는데, 각각의 경우 단위 테스트 시 비교를 위해 커스텀하였습니다.

DAO 또는 DTO를 설정할 때 가장 중요한 것은 클래스 내부 변수값을 대문자로 설정하면 안된다는 것입니다. 클래스 내부의 getOOO 메소드의 경우 주로 클래스 내 변수를 참조하는 역할을 하는데, 만약 메소드의 get 뒤에 붙는 이름과 변수명이 다를 경우 해당값을 리턴했을 때 get 메소드의 값이 같이 리턴됩니다.

package com.example.test.model.dao;

public class TestDTO {
    public int a; // 만약 public int A로 설정되어있다면 리턴 시 A, b, a 3개의 값이 리턴
    public int b;

    public TestDTO(int a, int b){
        this.a = a;
        this.b = b;
    }

    public int getA(){
        return a;
    }

    public int getB(){
        return b;
    }
}

위의 예시를 들어보겠습니다. 현재 a,b가 내부 변수로 존재하고 getA, getB 메소드가 존재합니다. 위의 경우 리턴 시 a,b 두 개의 값이 나타납니다. 하지만 위의 a 변수를 A로 교체한다면 리턴 시 A, b, a 3개의 값으로 나타납니다. 따라서 내부 변수명과 get 메소드를 정의할 때 소문자로 정의해서 리턴값을 조절하시면 되겠습니다.

package com.example.test.repository;

import com.example.test.model.UserDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.util.List;


@Repository
public class UserRepository {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public List<UserDTO> getUserInfoAll() {
        StringBuilder query = new StringBuilder();
        query.append("SELECT ID, name");
        query.append(" FROM User");
        query.append(" WHERE ID = 1;");
        return jdbcTemplate.query(query.toString(), (rs, rowNum) -> {
            UserDTO entity = new UserDTO(rs.getLong("ID"), rs.getString("name"));
            return entity;
        });
    }
}

다음으로 UserRepository 클래스를 작성합니다. JdbcTemplate 객체를 만들어 @Autowired로 의존성 주입 후 메소드 내에 실행할 쿼리와 쿼리 결과를 JdbcTemplate 객체에서 가져와 List로 저장합니다.

package com.example.test.service;

import com.example.test.model.UserDTO;

import java.util.List;

public interface UserService {

    public List<UserDTO> getUserInfoAll();

}

위의 코드는 UserService 인터페이스입니다.

package com.example.test.serviceImpl;

import com.example.test.model.UserDTO;
import com.example.test.repository.UserRepository;
import com.example.test.service.UserService;
import org.springframework.stereotype.Component;

import java.util.List;

@Component("userService")
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;
    public UserServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public List<UserDTO> getUserInfoAll() {
        List<UserDTO> users = userRepository.getUserInfoAll();
        return users;
    }
}

UserServiceImpl 클래스를 생성하여 UserService를 implements합니다. 내부에서 UserRepository의 메소드를 오버라이드하여 내용을 커스텀한 후 다시 리턴합니다.

package com.example.test.controller;

import com.example.test.model.UserDTO;
import com.example.test.service.UserService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class UserController {
    private final UserService userService;
    public UserController(UserService userService){ this.userService = userService; }

    @GetMapping("/user")
    public List<UserDTO> user(){
        return userService.getUserInfoAll();
    }
}

다음은 UserController입니다. UserController에선 UserService를 통해 UserRepository와 연결하여 쿼리 실행 결과를 해당 URI와 매핑합니다.

package com.example.test.repository;

import com.example.test.model.UserDTO;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.util.ArrayList;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(SpringExtension.class)
@SpringBootTest
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    public void userRepositoryTest(){
        // 예상 데이터를 직접 추가
        List<UserDTO> expectedListData = new ArrayList<>();
        expectedListData.add(
                new UserDTO(
                        Long.parseLong(String.valueOf(1)),
                        "테스트"
                )
        );
        Object[] expectedList = expectedListData.toArray();

        // 실제 데이터 가져오기
        Object[] actualList = userRepository.getUserInfoAll().toArray();

        // 두 값을 비교, 일치하지 않을 경우 에러 메시지 리턴
        assertArrayEquals(expectedList,actualList);
    }
}

이제부턴 정상적으로 작성했는지 테스트를 진행해보겠습니다. 위의 코드는 UserRepositoryTest 클래스로 UserRepository가 정상적으로 작동하는지 테스트를 진행합니다. 테스트 로직은 예상 데이터를 추가한 Array와 UserRepository에서 가져온 실제 데이터 Array가 같은지를 판단하여 같다면 테스트 통과, 다르다면 어떤 부분이 다른지 차이점으로 보여줍니다. 이 때 앞서 UserDTO에서 오버라이딩한 equals 코드가 없으면 객체 단위로 비교를 진행하기 때문에 내부 데이터를 같아도 객체가 달라 다른 데이터라고 결론을 내게 됩니다. 따라서 equals 코드를 오버라이딩 후 List로 받은 데이터를 Object Array로 변환한 후 assertArrayEquals를 진행하여 비교하시면 되겠습니다.

package com.example.test.service;

import com.example.test.model.UserDTO;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.util.ArrayList;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(SpringExtension.class)
@SpringBootTest
class UserServiceTest {

    @Autowired
    private UserService userService;

    @Test
    public void userServiceTest(){
        // 예상 데이터를 직접 추가
        List<UserDTO> expectedListData = new ArrayList<>();
        expectedListData.add(
                new UserDTO(
                        Long.parseLong(String.valueOf(1)),
                        "테스트"
                )
        );
        Object[] expectedList = expectedListData.toArray();

        // 실제 데이터 가져오기
        Object[] actualList = userService.getUserInfoAll().toArray();

        // 두 값을 비교, 일치하지 않을 경우 에러 메시지 리턴
        assertArrayEquals(expectedList,actualList);
    }
}

UserServiceTest 역시 같은 방식으로 진행합니다. 현재 예시는 UserRepository에서 가져온 데이터를 가공하지 않아 같은 데이터를 넣었지만 데이터 가공이 필요할 경우 여기서 테스트를 진행합니다.

테스트가 완료되면 다음과 같은 문구가 나타나며,

다음과 같이 정상적으로 서버가 작동하는 것을 확인하실 수 있습니다. 이상으로 오늘의 포스팅을 마치도록 하겠습니다!

profile
저는 상황에 맞는 최적의 솔루션을 깊고 정확한 개념의 이해를 통한 다양한 방식으로 해결해오면서 지난 3년 동안 신규 서비스를 20만 회원 서비스로 성장시킨 Software Developer 최민길입니다.

0개의 댓글