ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [1단계] Spring MVC 웹 앱 4일차. Spring JDBC 테스트와 계층 별 클래스 작업
    레거시/레거시-마이 셀파 리부트 2018. 6. 21. 14:37
    반응형

    *본 글은 필자가 진행한 프로젝트 '마이 셀파'의 작업 일기입니다. 재밌게 봐주세요. 코드와 작업 진행 사항은 https://github.com/gurumee92/MySelpaReboot 에서 확인해볼 수 있습니다.


    마이 셀파 리부트 1단계 Spring MVC를 활용한 웹 앱

    4일차 Spring JDBC 테스트와 계층 별 클래스 작업


    오늘은 3일차에 구축하였던 My SQL DB와 Spring JDBC를 통해 DB와 연결해서 앱에 적용시키는 테스트 작업을 진행하였습니다. 오늘은 실제적으로 앱에 적용하는게 아니라 테스트 앱을 만들어서 설계 단계에서 화면에 도달했을 때 어떤 기능을 구현해야 하는지 살펴보고 그에 따른 계층별로 모델, 레포지토리, 서비스를 간단하게 구현하였습니다. 본격적으로 작업하기 전에 Spring JDBC가 뭔지 간단하게 살펴보도록 하죠. Spring JDBC의 개념은 '스프링 철저 입문- 위키북스'라는 책을 참고하였습니다.


    간단하게 Spring JDBC 알아보기


    Spring JDBC란 스프링 프레임워크에서 제공하는 데이터 접근 모듈입니다. 단순하게 DB CRUD외에도 트랜잭션 관리, 접근 시 예외 처리 등의 다양한 기능들을 제공합니다. 제가 중점적으로 알아볼 것은 데이터 소스 연결 작업, 간단한 CRUD 작업입니다. 이제 데이터 소스라는 것을 알아보죠.


    데이터 소스란 애플리케이션이 DB에 접근하기 위한 추상화된 연결 방식, 즉 커넥션(java.sql.Connection)을 제공하는 역할을 하는 녀석을 지칭합니다. 데이터 소스를 사용하는 방법은 크게 세 가지 종류가 있습니다.


    • 애플리케이션 모듈이 제공하는 데이터 소스 
      Commons DBCP, Tomcat JDBC Connection Pool과 같이 서드파티가 제공하는 데이터 소스 혹은 DriverManagerDataSource 같이 스프링 프레임워크가 테스트 용도로 제공하는 데이터 소스를 빈으로 등록해서 사용하는 방법
    • 애플리케이션 서버가 제공하는 데이터 소스
      서버가 정의한 데이터 소스를 JNDI(Java Namming and Directory Interface)를 통해 가져와서 사용하는 방법
    • 내장형 데이터베이스를 사용하는 데이터 소스
      H2 같은 내장형 데이터베이스에 접근하는 데이터 소스를 사용하는 방법

    스프링 부트에서 스프링 JDBC와 데이터 소스를 연결하는 작업을 application.properties에 작성하면 됩니다. 이는 추후에 테스트 앱을 만들면서 제대로 살펴보겠습니다.


    Spring JDBC를 사용했을 때 개발자의 이점은 공통적이면서 반복되는 작업들을 대신 해준다는 것입니다. JDBC가 대신 해주는 작업은 다음과 같습니다.


    • 커넥션의 연결과 종료
    • SQL 문의 실행
    • SQL 문 실행 결과 행에 대한 반복 처리
    • 예외 처리

    따라서 개발자는 SQL문 정의, 파라미터 설정, SQL에서 결과를 가져온 후 레코드별로 필요한 처리만 하면 됩니다. 이제 이러한 처리를 어떻게 하는지 알아보죠. Spring JDBC는 JdbcTemplate이라는 클래스로 데이터 CRUD 작업을 처리합니다.


    JDBCTempalte CRUD 작업 시 주요 메소드

     메소드 명 

     설명 

     queryForObject

     하나의 결과 레코드 중 하나의 칼럼 값을 가져올 때 사용합니다. RowMapper 등과 매핑을 도와주는 클래스와 함께 사용하며 객체에 매핑합니다.

     queryForMap

     하나의 결과 레코드 정보를 Map 형태로 매핑할 수 있습니다.

     queryForList

     여러 개의 결과 레코드를 List로 Map<String, Object> 형태로 만들어서 반환해줍니다.

     query

     ResultSetExtractor, RowCallbackHandler와 같이 사용합니다. 

     update

     데이터를 변경하는 SQL INSERT, DELETE, UPDATE를 실행할 때 사용합니다.


    여기서 제가 주로 사용할 메소드는 다음과 같습니다.


    • queryForObject
    • queryForList
    • update

    어떻게 쓰는지는 데이터 소스를 연결하는 작업과 마찬가지로 테스트 앱을 작성하면서 알아보겠습니다. 


    테스트 앱 생성


    실무에서는 모듈 테스트는 Spring Test라는 모듈을 이용해서 유닛 테스트 등을 작업한다고 합니다. 그러나 저는 아직 스프링 테스트라는 모듈의 이해가 부족하기 때문에 따로 앱을 새로 만들어서 진행하였습니다. 테스트 앱을 작성할 때 IDE로 STS, 언어로는 자바로 간단하게 만들어보았습니다. 이제 테스트 앱을 만들어보죠. 앱의 작성 단계는 다음과 같습니다.

    1. File -> new -> other
    2. Spring Boot -> Spring Starter Project -> Next
    3. Name, Group, Artifact, Package명을 작성 후 Next
    4. 모듈 선택 (Web, MySQL, JDBC) 
    5. Finish

    그 후 프로젝트 구조를 이전과 똑같이 잡아주면 됩니다. 다만 Thymeleaf 모듈을 쓰지 않기 때문에 Templates 디렉토리 작업은 하지 않아도 됩니다.


    앱과 데이터 소스 연결


    자 이제 본격적으로 JDBC 모듈 설정을 해보겠습니다. 먼저 앞서 말했듯이 application.properties에 JDBC 설정을 해주어야 합니다.


    [프로젝트]/resource/application.properties

    spring.datasource.url=jdbc:mysql://localhost:3306/myselpa
    spring.datasource.username=root
    spring.datasource.password=비밀번호


    url에는 jdbc:[원하는 DB]://[DB 주소:포트]/[사용할 DB명]을 작성하면 됩니다. 저는 여기서 mysql과 제 컴퓨터 주소인 localhost 기본 포트번호인 3306을 적어주었습니다. myselpa는 아시겠지만 저번에 만들어 두었던 DB명입니다. 그리고 username과 password는 DB에서 사용한 유저명과 비밀번호를 입력해주시면 됩니다. 더 설정할 것이 있지만 이 정도만 해도 DB에 연결해서 CRUD 작업을 진행할 수 있습니다.


    계층 별 모델, 레포지토리, 서비스 클래스 만들기


    본격적으로 알아보기 전에 어떻게 할 것인지 정의해보도록 하겠습니다. 이전 포스트에서 계층형 아키텍처로 패키지를 나눈 것을 기억하시나요? 여기서는 도메인 계층 모델, 레포지토리, 서비스에 대한 설명을 한번 더 짚고 넘어가겠습니다.


     패키지 

     설명 

     모델

     DB 엔티티에 대한 POJO 클래스들을 모아둡니다.

     레포지토리

     DB와 연결해서 모델의 대한 CRUD 작업을 진행합니다.

     서비스

     레포지토리를 이용해서 데이터를 불러오고 비지니스 로직을 수행합니다. 예외처리는 여기서 진행합니다.


    이제 기능별로 필요한 모델 계층, 레포지토리 계층, 서비스 계층를 정의하고 레포지토리에서 Spring JDBC의 JdbcTemplate을 통해서 CRUD 작업을 진행하겠습니다. 본 포스팅에는 간단하게 유저에 대한 작업을 어떻게 하였는지만 살펴보겠습니다. 앱에서 유저 계층이 사용하는 서비스는 다음과 같습니다.

    1. 로그인 기능
    2. 회원가입 시 ID 중복 체크 기능
    3. 회원가입 기능

    이러한 기능들은 유저 서비스라는 클래스에서 앱에게 제공하게 될 기능들입니다. 이러한 서비스를 위해서는 DB에서 어떻게 데이터를 얻어와야 할까요?


     서비스 

     필요 정보 

     SQL 

     로그인

     사용자가 입력한 아이디와 비밀번호

     select * from user where id=[id] and pw=[pw]

     회원가입 시 ID 중복 체크 기능

     사용자가 입력한 아이디

     select * from user where id=[id] 

     회원가입

     사용자가 입력한 아이디, 비밀번호, 전화번호, 차 번호

     insert into user(id, pw, phone, carno) values([id], [pw], [phone], [carno])


    이러한 데이터 CRUD는 유저 레포지토리에 정의할 겁니다. 본격적으로 CRUD 작업을 하기 전에 이러한 작업들을 반환할 모델이 필요하겠죠? 그래서 유저 클래스를 만들겁니다. 회원 가입 시에 아이디, 비밀번호, 전화번호, 차 번호를 입력해줍니다. 이것들은 유저의 필드라고 볼 수 있습니다. 또한 마스터인지의 유무에 따라 화면 이동이 변하니까 유저 클래스 내에 마스터 여부를 반환하는 필드도 있어야겠죠? 그래서 저는 User클래스를 다음과 같이 정의하였습니다.


    [프로젝트]/domain/model/User.java


    package gurumee.domain.model;

    public class User {
        private final String id;
        private final String pw;
        private final String phone;
        private final String carNo;
        private final boolean isMaster;
        
        public User() {
            this.id="dummy";
            this.pw="dummy";
            this.phone="dummy";
            this.carNo="dummy";
            this.isMaster = false;
        }
        
        public User(String id, String pw, String phone, String carNo, boolean isMaster) {
            this.id = id;
            this.pw = pw;
            this.phone = phone;
            this.carNo = carNo;
            this.isMaster = isMaster;
        }
        
        public String getId() {
            return id;
        }

        public String getPw() {
            return pw;
        }

        public String getPhone() {
            return phone;
        }

        public String getCarNo() {
            return carNo;
        }

        public boolean isMaster() {
            return isMaster;
        }
    }


    그 후 유저 데이터 CRUD를 위한 UserRepository를 정의하였습니다. 테스트는 오로지 READ 작업만을 진행하였습니다. 일단 INSERT, DELETE 등은 DB를 더럽힐 수 있기 때문에 추후 작업을 진행할 예정입니다.


    [프로젝트]/domain/repository/user/UserRepository.java


    package gurumee.domain.repository.user;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Repository;

    import gurumee.domain.model.User;

    @Repository
    public class UserRepository {
        @Autowired
        JdbcTemplate jdbcTemplate;
        
        public User findUserById(String id) {
            String sql = "select * from user where id='"+ id + "'";
            return jdbcTemplate.queryForObject(sql, (rs, rowNum)->{
                if (rowNum == 0)
                    return new User();
                
                String userId = rs.getString("id");
                String userPw = rs.getString("pw");
                String phone = rs.getString("phone");
                String carNo = rs.getString("carno");
                boolean isMaster = rs.getBoolean("is_master");
                
                User user = new User(userId, userPw, phone, carNo, isMaster);
                return user;
            });
        }

        public User findUserByIdAndPw(String id, String pw) {
            String sql = "select * from user where id='"+ id + "' and pw='" + pw + "'";
            return jdbcTemplate.queryForObject(sql, (rs, rowNum)->{
                
                String userId = rs.getString("id");
                String userPw = rs.getString("pw");
                String phone = rs.getString("phone");
                String carNo = rs.getString("carno");
                boolean isMaster = rs.getBoolean("is_master");
                
                User user = new User(userId, userPw, phone, carNo, isMaster);
                return user;
            });
        }

    }


    먼저 보실 것은 이 부분입니다.


    @Repository
    public class UserRepository {
        @Autowired
        JdbcTemplate jdbcTemplate;
    ...
    }


    코드 설명

    • @Repository 
      이 애노테이션은 클래스가 레포지토리라는 것을 명시해 줍니다. 앱에서 CRUD 작업을 수행하며 나중에 @Service가 붙은 클래스들에게 사용됩니다.
    • @Autowired
      이 애노테이션은 앱이 빌드 시에 자동으로 주입해준다는 뜻입니다. 이렇게 해두면 클래스 내에서 new 연산자 없이 이 애노테이션이 붙은 필드들을 사용할 수 있습니다. 일단은 Spring JDBC가 제공하는 JdbcTemplate 클래스를 사용하려면 이 애노테이션을 붙여야 되는구나 정도로만 이해해 주세요.
    • JdbcTemplate jdbcTemplate 
      실제로 앱에서 DB를 접근해주는 녀석입니다. 

    앞에서 Spring JDBC는 커넥션 연결/해제등의 일련의 작업을 해주고 개발자로 하여금 SQL 정의, 파라미터 설정, 레코드 처리만 신경쓰도록 만든다고 언급하였습니다. 실제로 어떤지 확인해보겠습니다.


    //파라미터 설정 String id
    public User findUserById(String id) {
    //SQL 정의
        String sql = "select * from user where id='"+ id + "'";
    //레코드 처리
        return jdbcTemplate.queryForObject(sql, (rs, rowNum)->{
            if (rowNum == 0)
                return new User();
                
            String userId = rs.getString("id");
            String userPw = rs.getString("pw");
            String phone = rs.getString("phone");
            String carNo = rs.getString("carno");
           boolean isMaster = rs.getBoolean("is_master");
                
            User user = new User(userId, userPw, phone, carNo, isMaster);
            return user;
      });
    }


    이 메소드는 아이디로 유저 클래스를 검색하는 메소드입니다. 이 메소드 안에서 커넥션 연결 및 해제, 예외 처리 등의 작업을 찾아볼 수 없습니다. 다만 jdbcTemplate에 queryForObject라는 메소드에 sql을 싫어넣으면 DB에서 해당하는 정보를 열 별로 정보가 담겨져 오는구나라는 걸 알 수 있습니다. 


    참고! queryForObject 메소드의 두번째 파라미터

    queryForObject와 함께 쓰이는 매핑 인터페이스로는 다음의 3가지가 있습니다.


    • RowMapper
    • ResultSetExtractor
    • RowCallbackHandler

    이 중 이 두번째 파라미터인 람다가 구현하는 인터페이스는 RowMapper입니다. 저는 단 하나의 엔티티를 반환하는 것이라면 queryForObjec와 RowMapper 조합으로 이 작업을 진행하였습니다. 미숙한 제 눈에는 이게 제일 코드가 명확해 보여서요..


    현재 Repository에서 제공하는 두개의 메소드는 파라미터와 SQL만 살짝 다를뿐이므로 Repository의 설명을 여기서 마치겠습니다. 이제 실질적으로 앱에게 서비스를 제공하는 UserService 클래스를 만들어보죠.


    [프로젝트]/domain/service/user/UserService.java

    package gurumee.domain.service.user;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import gurumee.domain.model.User;
    import gurumee.domain.repository.user.UserRepository;

    @Service
    @Transactional
    public class UserService {

        @Autowired
        UserRepository userRepository;
        
        public User findUser(String id) {
            User user;
            try {
                user = userRepository.findUserById(id);
            }catch(Exception e) {
                user = new User();
            }
            return user;
        }
        
        public User findUser(String id, String pw) {
            
            User user;
            try {
                user = userRepository.findUserByIdAndPw(id, pw);
            }catch(Exception e) {
                user = new User();
            }
            return user;
        }
    }


    여기서 먼저 이 부분을 살펴 보죠.


    @Service
    @Transactional
    public class UserService {

        @Autowired
        UserRepository userRepository;
    ....
    }


    코드 설명

    • @Service
      먼저 레포지토리와 마찬가지로 서비스 계층의 클래스들은 @Service라는 애노테이션을 붙여야 합니다. 후에 컨트롤러에 주입되어 사용됩니다.
    • @Transactional
      이 서비스에서 메소드 작업을 할 때 스프링 자체에서 트랜잭션을 걸게 해준다는 뜻입니다.

    그리고 서비스 계층은 DB에서 데이터를 가져오기 위해서 개발자가 정의해 둔 레포지토리 계층의 클래스를 가져다 씁니다. 이번엔 메소드를 한 번 살펴보죠.


    public User findUser(String id) {
        User user;
        try {
            user = userRepository.findUserById(id);
        }catch(Exception e) {
            user = new User();
        }
        return user;
    }


    여기서 한 가지 알아둘 점이 있습니다. queryForObject는 sql의 결과 레코드가 0건이면 예외를 발생시킨다는 점입니다. 서비스 계층은 비지니스 로직을 포함하기 때문에 에러처리도 여기서 해주게 됩니다. 


    참고!

    queryForMap 역시 결과 레코드가 0건이라면 에러를 발생시킵니다. 반면에 queryForList는 에러 없이 빈 리스트를 반환해줍니다. 


    자 이제 앱에서 서비스를 한 번 써봅시다. 간단하게 테스트 해보기 위하여 LoginController를 다음과 같이 정의하였습니다.


    [프로젝트]/app/login/LoginController

    package gurumee.app.login;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;

    import gurumee.domain.model.User;
    import gurumee.domain.service.user.UserService;

    @RestController
    public class LoginController {
        @Autowired
        UserService userService
            
        
        @RequestMapping("/")
        String getLogin() {
            String id = "gurumee2";
            String pw = "2";
            User user = userService.findUser(id, pw);
            return user.getId() + " " + user.getPw() + " " + user.getPhone() + " " + user.getCarNo();
        }
    }


    이 컨트롤러는 RestController입니다. 원래 RESTful 서버를 만들 때 쓰는 컨트롤러인데 테스트하기 위해서 이렇게 만들었습니다.  이제 이 앱의 / URL을 치면 다음과 같은 결과가 나올 겁니다.




    이렇게 해서 오늘은 간단하게 Spring JDBC를 통해서 DB에서 데이터를 획득하고 앱에서 계층별로 필요한 클래스들을 만드는 작업을 진행하였습니다. 길고 두서 없는 글을 읽으시느라 고생하셨습니다! 오늘은 제가 개인적인 사정이 생겨서 이렇게 야밤에 올리게 되었습니다. 내일 아니 오늘은 Spring JDBC에서 Spring Data JPA로 변환하는 작업을 진행할 예정입니다. 다른 계층에 대한 코드는 저의 깃헙에 올려놓도록 하겠습니다.

Designed by Tistory.