ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [1단계] Spring MVC 웹 앱 5일차. Spring Data JPA 적용
    레거시/레거시-마이 셀파 리부트 2018. 6. 22. 16:17
    반응형

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


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

    5일차 Spring Data JPA 적용


    오늘은 4일차에 진행했던 Spring JDBC를 통해 DB에서 데이터를 읽어오는 부분을 Spring Data JPA로 변경하고 거기에 맞게 계층 별 클래스들을 다시 재 정의하는 작업을 진행하였습니다. 저는 단순하게 이 모듈을 써보고 싶어서 프로젝트에서 진행하였으나 JPA를 잘 모르시는 분들은 JDBC로 작업하는 것을 추천드립니다. 죽는 줄 알았어요...본격적으로 작업을 진행하기 앞서 Spring JPA에 대해 알아보도록 하죠.


    Spring Data JPA란?


    이것을 알아보기 전에 몇 가지 용어들의 정의가 필요합니다.


    용어 정의


    • ORM(Object-Relation-Mapping)
      RDBMS에 데이터를 읽고 쓰는 처리를 객체에 데이터를 읽고 쓰는 방식으로 구현하는 방법
    • 하이버네이트
      자바 용 ORM의 원조격.
    • JPA(Java Persistence API)
      하이버네이트의 사상을 이어 받아 만들어졌고 현재 자바의 표준 ORM 도구
    • Spring Data JPA
      Spring JDBC 처럼 로직과 상관 없이 일어나는 반복적인 작업들을 Spring에서 지원해주고 개발자로 하여금 로직에 집중할 수 있게 만들어주는 스프링 프레임워크 데이터 모듈.

    그렇다면 왜 Spring Data JPA를 쓰는 걸까요? 이 이유를 알기 위해서는 ORM을 왜 쓰는지 부터 알아야 합니다. 책 '스프링 철저 입문'의 내용을 요약해보면 JPA 등의 ORM은 RDBMS에서 데이터를 읽는 것은 객체 지향 언어인 자바에게 맞지 않아서 생기는 불편함을 제거하기 위해서입니다. Spring JPA는 데이터를 접근하는 Repository 클래스들을 구현하는데 걸리는 부하를 최소화하기 위해서 쓰여지고 있습니다. 또한 JPA는 배타적 제어라는 것을 통해 동시에 여러 트랜잭션이 실행되는 데이터 갱신 처리를 도와주는 기능을 가지고 있는데 이 것 역시 쓰는 이유 중 하나입니다. 이제 본격적으로 코드 작업을 진행해보겠습니다. 4일차와 마찬가지로 테스트 앱을 작성해서 JPA의 기능들을 프로젝트에 적용해 보았습니다..


    테스트 앱 생성과 설정


    4일차와 같은 순서로 작업을 진행합니다. 다만, 오늘은 모듈 선택에서 JDBC, MySQL, Web에서 JDBC 대신 JPA를 선택하고 프로젝트를 생성하고 이전과 같이 패키지 구조를 잡아주었습니다.


    참고!

    프로젝트 생성과 패키지 구조 잡는 것은 4일 차 포스팅을 참고해주세요.


    JPA 역시 JDBC와 마찬가지로 application.properties 파일을 통하여 DB와 연결해주는 작업을 해야 합니다. 저는 다음과 같이 설정해주었습니다.


    [프로젝트]/src/main/resources/application.properties


    //DB 연결 작업
    spring.jpa.database=mysql
    spring.datasource.url=jdbc:mysql://localhost:3306/myselpa
    spring.datasource.username=root
    spring.datasource.password=비밀번호
    //JPA 설정
    spring.jpa.hibernate.ddl-auto=validate            //런타임 전에 @Entity 클래스와 DB 테이블들을 비교하여 적적한지 판단
    spring.jpa.properties.hibernate.format_sql=true //JPA가 자동으로 실행하는 SQL을 보기 좋게 출력
    spring.datasource.sql-script-encoding=UTF-8
    logging.level.org.hibernate.SQL=DEBUG
    logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE


    4일차 작업 되돌아보기


    사실 이전 포스팅에서 생략한 작업 중에 중요한 것들을 언급하지 않아서 짚고 넘어가겠습니다. 4일차에서 모델 계층의 클래스들을 작업할 때 저는 DB의 테이블과 똑같이 클래스를 만드는 것이 아닌 실제 애플리케이션에서 표현되는 정보들을 모아서 클래스로 만들었습니다. 이번 포스팅에는 주차장과 예약 기능에 대해서 4일차에서는 어떻게 만들고 적용했는지 살펴보도록 하죠.


    테이블 parking_lot

     필드

     타입

     설명

     no

     int 

     pk, auto_increment 

     name

     varchar

     not null

     total_sector_cnt

     int

     not null, default 4


    참고!

    이전에는 sectorCnt로 필드를 지정해주었습니다만, JPA는 내부적으로 SQL을 실행하는데 필드를 이렇게 지으면 자동으로 sector_cnt로 바꾸어서 쿼리를 날립니다. 그래서 런타임 이전에 DB 테이블과 @Entity 모델 클래스들과 매핑되지 않는다는 오류를 발생시킵니다. 따라서 JPA가 읽을 수 있도록, 또한 필드가 더 정확한 정보를 가진 이름으로 변경해주었습니다.


    이번에는 화면에 보이는 주차장 정보를 살펴보죠.

    우선 이름과 예약한 유저의 수, 주차장 전체 주차 공간의 수 그리고 이걸 클릭하게 되면 주차장 번호로 상세 정보로 표시해주어야 합니다. 따라서 ParkingLot 클래스는 다음과 같이 정의 되었습니다.

     

    [프로젝트]/src/main/java/.../domain/model/ParkingLot.java


    package gurumee.domain.model;

    public class ParkingLot {
        private final int no;
        private final String name;
        private final int currentUserCnt;
        private final int totalSectorCnt;
        
        public ParkingLot() {
            this.no = -1;
            this.name = "dummy";
            this.currentUserCnt = -1;
            this.totalSectorCnt = -1;
        }

        public ParkingLot(int no, String name, int cuurentUserCnt, int totalSectorCnt) {
            super();
            this.no = no;
            this.name = name;
            this.currentUserCnt = cuurentUserCnt;
            this.totalSectorCnt = totalSectorCnt;
        }

        public int getNo() {
            return no;
        }

        public String getName() {
            return name;
        }
        
        public int getCurrentUserCnt() {
            return currentUserCnt;
        }

        public int getTotalSectorCnt() {
            return totalSectorCnt;
        }

        @Override
        public String toString() {
            return "ParkingLot [no=" + no + ", name=" + name + ", currentUserCnt=" + currentUserCnt + ", totalSectorCnt=" + totalSectorCnt + "]";
        }
    }


    어떻게 보면 제가 짰던 SQL에 결과 물을 클래스로 만들었다고도 볼 수 있겠습니다. 다음은 3일차에 만들어두었던 시스템의 전체 주차장을 찾는 쿼리문입니다.


    select p.no , name , COUNT(lot_no) as "current_user_cnt", total_sector_cnt
    from reservation as r inner join parking_lot as p
    on r.lot_no=p.no
    group by p.no;


    SELECT 구에 씌여진 열의 이름들이 클래스의 필드들과 일치하는 것을 볼 수 있습니다. 이번에는 예약 정보를 나타내는 DB 테이블 reservation과 화면에 나타날 정보들, 그리고 클래스 Reservation을 살펴보죠.


    테이블 reservation

     필드 

     타입 

     설명 

     no

     int

     pk, auto_increment

     user_id

     varchar

     fk, not null

     lot_no

     int

     fk, not null

     sector

     int

     not null


    이번에는 마스터 유저의 주차장 검색 페이지입니다. 잘 생각해보면 이 화면에서 예약 정보 중 가장 많은 정보가 표시 됩니다.

    그림에서 표시한 것처럼 주차장의 이름, 유저의 ID 그리고 주차한 구역이 나타납니다. 또 한가지! 그냥 유저에서는 마이 페이지에서 자신의 차가 주차될 경우 '차량 번호'와 주차장 이름이 뜨게 됩니다. 그리고 이전에 작업해 두었던 쿼리에서 예약 정보를 참조할 때 기본키인 no가 활용되기 때문에 Reservation 클래스를 다음과 같이 짰습니다.


    [프로젝트]/src/main/java/.../domain/model/Reservation.java


    package gurumee.domain.model;

    public class Reservation {
        private final int reservationNo;
        private final String userId;
        private final String userCar;
        private final String parkingLotName;    
        private final int parkingAtSectorNo;
        
        
        public Reservation(int reservationNo, String userId, String userCar, String parkingLotName, int parkingAtSectorNo) {
            this.reservationNo = reservationNo;
            this.userId = userId;
            this.userCar = userCar;
            this.parkingLotName = parkingLotName;
            this.parkingAtSectorNo = parkingAtSectorNo;
        }
        
        public Reservation() {
            this(-1, "dummy", "dummy", "dummy", -1);
        }

        public int getReservationNo() {
            return reservationNo;
        }

        public String getUserId() {
            return userId;
        }

        public String getParkingLotName() {
            return parkingLotName;
        }

        public int getParkingAtSectorNo() {
            return parkingAtSectorNo;
        }

        public String getUserCar() {
            return userCar;
        }

        @Override
        public String toString() {
            return "Reservation [reservationNo=" + reservationNo + ", userId=" + userId + ", userCar=" + userCar
                    + ", parkingLotName=" + parkingLotName + ", parkingAtSectorNo=" + parkingAtSectorNo + "]";
        }
    }


    레포지토리와 서비스는 3일차에 작성해둔 쿼리를 토대로 다 만들어졌습니다. 그리고 이들의 SQL은 꽤 복잡합니다. 다음은 사용할 SQL 전문입니다.


    참고! 

    이전과 조금 다를겁니다. 결과가 아예 틀리게 나오는 쿼리문도 있고 JDBC를 활용할 때 SQL이 이상하게 안 돌아가는 쿼리도 있어서 바꿨습니다.


    /*유저 정보*/
    /*현재 서비스를 이용하는 모든 고객*/
    select r.no, u.id, u.carno, p.name, r.sector
    from user as u inner join reservation as r
    on u.id = r.user_id, parking_lot as p
    where r.lot_no=p.no;

    /*검색 결과에 해당하는 주차장의 모든 고객*/
    select r.no, u.id, u.carno, p.name, r.sector
    from user as u inner join reservation as r
    on u.id = r.user_id, parking_lot as p
    where r.lot_no=p.no AND p.name LIKE '%산%';


    /*주차장 별 이용하는 모든 고객 */
    select r.no, u.id, u.carno, p.name, r.sector
    from user as u inner join reservation as r
    on u.id=r.user_id, parking_lot as p
    where r.lot_no=p.no and p.no=1;

    /*주차장 정보*/

    /*전체 주차장 검색 결과*/
    select p.no , name , COUNT(lot_no) as "current_user_cnt", total_sector_cnt
    from reservation as r inner join parking_lot as p
    on r.lot_no=p.no
    group by p.no;

    /*검색 결과에 따른 주차장 검색 결과*/
    select p.no , name , COUNT(lot_no) as "current_user_cnt", total_sector_cnt
    from reservation as r inner join parking_lot as p
    on r.lot_no=p.no
    group by p.no;
    having p.name like "%해%";

    /*예약 정보*/
    /*전체 예약 정보*/
    select r.no, u.id, u.carno, p.name, r.sector
    from user as u inner join reservation as r
    on u.id = r.user_id, parking_lot as p
    where r.lot_no=p.no

    /*유저 아이디에 따른 예약 정보*/
    select r.no, user_id as 'id', u.carno, p.name, r.sector
    from reservation as r inner join parking_lot as p
    on r.lot_no=p.no
    where user_id='gurumee2';

    /*주차장 번호 별 예약 정보*/
    select r.no, u.id, u.carno, p.name, r.sector
    from user as u inner join reservation as r
    on u.id = r.user_id, parking_lot as p
    where r.lot_no=p.no and p.no=1;

    /*검색된 주차장 별 예약 정보*/
    select r.no, u.id, u.carno, p.name, r.sector
    from user as u inner join reservation as r
    on u.id = r.user_id, parking_lot as p
    where r.lot_no=p.no and p.name LIKE '%산%';


    그리고 SQL마다 레포지토리, 서비스 별로 메소드가 정의된 것을 확인할 수 있을겁니다. 예를 들어서 ReservationRepository클래스를 살펴보죠.


    package gurumee.domain.repository.reservation;

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

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

    import gurumee.domain.model.Reservation;

    @Repository
    public class ReservationRepository {

        @Autowired
        JdbcTemplate jdbcTemplate;
        
        public Reservation findReservationByUserId(String userId) {
            
            String sql = "select r.no, user_id as 'id', u.carno, p.name, r.sector from reservation as r inner join parking_lot as p on r.lot_no=p.no where user_id='"+ userId +"'";
            
            return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> {
                int resvNo = rs.getInt("no");
                String id = rs.getString("id");
                String carNo = rs.getString("carno");
                String name = rs.getString("name");
                int sector = rs.getInt("sector");
                
                Reservation info = new Reservation(resvNo, id, carNo, name, sector);
                return info;
            }); 
        }
        public List<Reservation> findAllReservations(){
            
            String sql = "select r.no, u.id, u.carno, p.name, r.sector from user as u inner join reservation as r on u.id = r.user_id, parking_lot as p where r.lot_no=p.no";
            List<Map<String, Object>> resList = jdbcTemplate.queryForList(sql);
            
            List<Reservation> list = new ArrayList<>();
            
            for (Map<String, Object> res : resList) {
                int resvNo = (Integer)res.get("no");
                String id = (String)res.get("id");
                String carNo = (String)res.get("carno");
                String lotName = (String)res.get("name");
                int sectorNo = (Integer)res.get("sector");
                
                Reservation reservation = new Reservation(resvNo, id, carNo, lotName, sectorNo);
                list.add(reservation);
            }
            
            return list;
        }
        
        //�˻� ���ڿ��� ���� ���� ����
        //�̷��� �ص� �ǰ� ���͸��ص� �ɵ�
        public List<Reservation> findResevationsByString(String search){
            
            String sql = "select r.no, u.id, u.carno, p.name, r.sector from user as u inner join reservation as r on u.id = r.user_id, parking_lot as p where r.lot_no=p.no and p.name LIKE '%" + search + "%'";
            List<Map<String, Object>> resList = jdbcTemplate.queryForList(sql);
            
            List<Reservation> list = new ArrayList<>();
            
            for (Map<String, Object> res : resList) {
                int resvNo = (Integer)res.get("no");
                String id = (String)res.get("id");
                String carNo = (String)res.get("carno");
                String lotName = (String)res.get("name");
                int sectorNo = (Integer)res.get("sector");
                
                Reservation reservation = new Reservation(resvNo, id, carNo, lotName, sectorNo);
                list.add(reservation);
            }
            
            return list;
        }
        
        public List<Reservation> findReservationsByLotNo(int lotNo){
            
            String sql = "select r.no, u.id, u.carno, p.name, r.sector from user as u inner join reservation as r on u.id = r.user_id, parking_lot as p where r.lot_no=p.no and p.no=" + lotNo;
            List<Map<String, Object>> resList = jdbcTemplate.queryForList(sql);
            
            List<Reservation> list = new ArrayList<>();
            
            for (Map<String, Object> res : resList) {
                int resvNo = (Integer)res.get("no");
                String id = (String)res.get("id");
                String carNo = (String)res.get("carno");
                String lotName = (String)res.get("name");
                int sectorNo = (Integer)res.get("sector");
                
                Reservation reservation = new Reservation(resvNo, id, carNo, lotName, sectorNo);
                list.add(reservation);
            }
            
            return list;
        }  
    }


    각 쿼리마다 필요한 메소드를 정의해 주었습니다. 이 말은 표현해야 할 정보가 많아질 수록 그만큼 새로운 SQL과 메소드들을 작성해야 한다는 뜻입니다. JPA를 사용해본 결과 이러한 쿼리 작업을 9개에서 4개로 줄여주었습니다. 실무에서는 훨씬 더 많은 양의 작업을 줄여줄 것으로 예상됩니다. 이제 JPA 적용 작업을 진행하도록 하겠습니다.


    JPA 적용 작업


    우선 JPA를 적용하기 전에 테이블 간 관계를 알아야 합니다. 그래야 모델 계층의 클래스들, 즉 @Entity 클래스 적용이 쉽습니다. 테이블 관계가 무엇인지 살펴보려면 어떤 테이블이 어떤 테이블을 참조하는지 여부에 따라 달라집니다. 우선 제가 시스템에서 정의한 테이블끼리의 관계는 다음과 같습니다.


    • 유저 -> 예약 (단방향, 일대일)
    • 주차장 -> 예약(단방향, 일대다)

    유저별 예약 건수는 1개입니다. 이는 이들의 관계가 일대일임을 뜻합니다. 그리고 유저의 정보가 바뀌면 예약 정보 역시 업데이트 됩니다. 그러나 DB에서 예약 정보를 통해서 유저의 정보를 바꿀 수 없기 때문에 단방향 관계라는 것을 알 수 있습니다. 주차장도 마찬가지이만 주차장에 따라 예약 정보는 여러건이 존재할 수 있습니다. 따라서 주차장과 예약 테이블의 관계는 단방향 일대다 관계입니다. 이제 테이블 간 관계를 알았으니 모델 계층의 작업을 진행하겠습니다. 우선 유저 클래스입니다.


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

    package gurumee.domain.model;

    import java.io.Serializable;

    import javax.persistence.CascadeType;
    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.FetchType;
    import javax.persistence.Id;
    import javax.persistence.OneToOne;
    import javax.persistence.Table;

    @Entity
    @Table(name="user")
    public class User implements Serializable{
        
        @Id
        @Column(name="id")
        private String id;
        
        @Column(name="pw")
        private String pw;
        
        @Column(name="phone")
        private String phone;
        
        @Column(name="carno")
        private String carNo;
        
        @Column(name="is_master")
        private Boolean isMaster;

    @OneToOne(fetch=FetchType.LAZY,
                 mappedBy="user",
                 cascade=CascadeType.ALL)
        private Reservation reservation;
        
    //...
        //GET-SET, toString 메소드 존재
    }


    코드 설명

    • @Entity 
      JPA로 테이블과 클래스를 매핑함을 알려주는 애노테이션입니다.
    • @Table(name="user")
      DB에서 어떤 테이블을 참조할 것인지 name은 테이블 이름입니다.
    • @Id
      기본키 필드와 클래스의 멤버 필드를 매핑시킵니다.
    • @Column(name='')
      이름에 해당하는 열과 필드를 매핑시킵니다.
    • @OneToOne
      일대일 관계의 엔티티를 매핑시킬 때 쓰는 애노테이션입니다. mappedBy 는 Reservation 클래스의 필드명입니다.


    JPA Entity 클래스는 약간의 조건이 더 붙습니다.


    1. Serializable을 구현할 것.(반드시는 아니지만 확장성을 위해 붙여줍니다.)
    2. 필드가 final이 안되게 할 것.
    3. get/set 메소드 구현할 것
    4. 기본형 타입 대신 Wrapper 클래스를 사용할 것

    참고!

    저도 왜 그런지는 모르겠습니다만, 클래스를 매핑시킬 때 기본 생성자로 클래스를 생성한 후 이들의 set 메소드를 통해서 DB에서 값을 설정해주는 것 같습니다.


    여기서 크게 살펴볼 점은 User 클래스 내부에 Reservation 클래스가 있다는 것입니다. 관계를 가지는 테이블들을 각각 매핑하는 엔티티 클래스들을 포함 관계를 만들 수 있습니다. 이것의 이점은 추후 논의하도록 하죠. 계속해서 ParkingLot 클래스입니다.


    [프로젝트]/src/main/java/.../domain/model/ParkingLot.java


    package gurumee.domain.model;

    import java.io.Serializable;
    import java.util.List;

    import javax.persistence.CascadeType;
    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.FetchType;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;
    import javax.persistence.OneToMany;
    import javax.persistence.Table;

    @Entity
    @Table(name="parking_lot")
    public class ParkingLot implements Serializable{

        @Id
        @GeneratedValue(strategy=GenerationType.IDENTITY)
        @Column(name="no")
        private Integer no;
        
        @Column(name="name")
        private String name;
        
        @Column(name="total_sector_cnt",
                columnDefinition="int default 4")
        private Integer totalSectorCnt;
        
        @OneToMany(fetch=FetchType.LAZY,
                 mappedBy="parkingLot",
                 cascade=CascadeType.ALL)
        private List<Reservation> reservationList;

        //GET-SET, toString
    }


    코드 설명

    • @GeneratedValue(...)
      DB에서 AUTO_INCREMENT 속성의 필드들을 매핑시킬 때 이 애노테이션을 붙입니다. Defult로는 GenerationType.AUTO 가 붙지만 MySQL 연결 시에 IDENTITY를 하지 않으면 에러가 떠서 설정해 주었습니다.
    • calumnDefinition
      이것은 필드의 디폴트 값이나 추가 적인 설정이 필요할 때 설정해주는 애노테이션 속성입니다.
    • @OneToMany
      일대다 관계를 나타내주는 애노테이션입니다. SQL에서는 객체 간 표현할 수 없었던 주차장 클래스에서 예약 클래스를 리스트로 갖게 되었습니다.

    이제 Reservation 클래스를 살펴보겠습니다.


    [프로젝트]/src/main/java/.../domain/model/Reservation.java


    package gurumee.domain.model;

    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;
    import javax.persistence.JoinColumn;
    import javax.persistence.ManyToOne;
    import javax.persistence.OneToOne;
    import javax.persistence.Table;

    @Entity
    @Table(name="reservation")
    public class Reservation {
        
        @Id
        @GeneratedValue(strategy=GenerationType.IDENTITY)
        @Column(name="no")
        private Integer no;
        
        @OneToOne
        @JoinColumn(name="user_id")
        private User user;
        
        @ManyToOne
        @JoinColumn(name="lot_no")
        private ParkingLot parkingLot;
        
        @Column(name="sector")
        private Integer sector;

    //GET-SET, toString
    }


    코드 설명

    • @JoinColumn
      매핑된 테이블 필드를 참조하여 해당 엔티티 클래스를 포함합니다.
    • @ManyToOne
      다대일 관계를 나타냅니다. 예약 테이블 입장에서는 주차장과 자신의 관계는 다대일입니다. 

    자 이제 Repository를 살펴봅시다. JPA를 사용할 때 레포지토리 계층은 인터페이스로 만들며 Interface JPARepository<T, P>를 상속해야 합니다. 여기서 T는 매핑될 클래스를 P는 해당 클래스의 기본키의 타입을 지정해줍니다. UserRepositroy는 다음과 같습니다.


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


    package gurumee.domain.repository.user;

    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.data.jpa.repository.Query;
    import org.springframework.data.repository.query.Param;
    import org.springframework.stereotype.Repository;

    import gurumee.domain.model.User;

    @Repository
    public interface UserRepository extends JpaRepository<User, String> {
        @Query("SELECT u FROM User u WHERE u.id = :id AND u.pw = :pw")
        User findUserByIdAndPw(@Param("id")String id,
                 @Param("pw")String pw);
    }


    코드 설명 

    • @Query 
      SQL이 아닌 JPQL을 작성합니다. SQL을 작성할 줄 알면 거의 작성이 쉽습니다. 쿼리를 적용시킬 테이블은 엔티티 클래스 명을 지정해주면 됩니다. 그리고 조건 검색을 위해서 파라미터를 지정해줄 수 있습니다. 
    • ":파라미터"
      문자열 속에서 위의 형식처럼 된 :id, :pw는 메소드의 파라미터로 지정될 아이들입니다. 최후에는 이 파라미터 값이 :id 혹은 :pw 자리에 적용됩니다.
    • @Param
      JPQL로 보내줄 파라미터입니다. 속성으로 문자열로 :[] 해당하는 문자열을 넣어주면 됩니다.
      • :id -> @Param("id")
      • :pw -> @Param("pw")

    참고1. 다른 방법으로 메소드 정의

    다른 방법으로 메소드 이름으로 자체적으로 쿼리문을 설정해주는 방법이 있습니다. 다만 저는 이 방식을 사용하지 않기 때문에 넘어갑니다. 


    참고2. findAll(), findById()

    아이디로 검색하거나 DB 전체의 리스트를 얻기 위해서 쿼리를 따로 작성할 필요는 없습니다. 왜냐하면 JPARepository 인터페이스가 이미 그 메소드에 대한 구현을 위임시켰기 때문입니다.


    JDBC로 작업하는 것보다 훨씬 간단하고 빠르게 레포지토리를 구현할 수 있었습니다. 이제 ParkingLotRepository입니다.


    [프로젝트]/src/main/java/.../domain/repository/parkinglot/ParkingLotRepository.java


    package gurumee.domain.repository.parkinglot;

    import java.util.List;

    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.data.jpa.repository.Query;
    import org.springframework.data.repository.query.Param;
    import org.springframework.stereotype.Repository;

    import gurumee.domain.model.ParkingLot;

    @Repository
    public interface ParkingLotRepository extends JpaRepository<ParkingLot, Integer> {
        
        @Query("SELECT p FROM ParkingLot p WHERE p.no = :no")
        public ParkingLot findParkingLotByNo(@Param("no")int no);
        
        @Query("SELECT p FROM ParkingLot p WHERE p.name LIKE CONCAT('%',:search,'%')")
        public List<ParkingLot> findParkingLotListByString(@Param("search")String search);
    }


    서비스 계층과 컨트롤러 작업은 4일차 작업과 비슷하기 때문에 설명은 따로 하지 않겠습니다. 다만 레포지토리에서 메소드가 적어진 만큼 서비스 계층에서 정의할 메소드도 적습니다. 또한 유저 혹은 주차장 정보만 찾을 수 있다면 해당 클래스의 예약 정보에 접근해서 정보를 찾을 수 있기 때문에 도메인 계층에서 reservation의 역할이 상당 수 줄어듭니다. 그래도 insert, delete 작업은 이 아이를 통해서 해야 하기 때문에 남겨둘 예정입니다.


    참고!

    저는 테스트를 위해 작성해두었지만 여기에는 쓰지 않겠습니다. 궁금하시면 깃헙의 코드를 참고해주세요! 


    긴 글 읽으시느라 고생하셨습니다!  이렇게 해서 오늘 작업도 무사히 마칠 수 있었습니다.  JPA의 이해가 부족한 터라 오류가 나도 무슨 오류인지 잡기가 힘들어서 죽을뻔 했다는.... 저처럼 잘 모르시다면 추천하지는 않습니다. 하지만 적용했을 때 계층 별 클래스들을 작업하는데 있어 생산성 확보와, 코드의 간결함은 이루 말할 수 없네요 고생한 보람이 있다. 6일차에는 이 JPA 작업한 클래스들을 실제로 앱에 적용하여 코틀린으로 바꿔보고 그 다음 Spring MVC와 Thymeleaf를 통해서 화면에 따라 앱을 작성하는 작업을 진행해보겠습니다.

Designed by Tistory.