항상 어떤 강의를 듣던지 이런 생각을 하게 되는 것 같습니다.
정말! 신기하고 재밌는데 어떻게 뭘 해보고는 싶은데 나는 항상 따라만 치고 포스팅만 하고 혼자 하라고 하면 아무것도 못하겠어!
이런 생각이 JPA를 하면서 찾아왔기에 허접한 작품이 될지는 몰라도 용기내서 스스로 게시판 프로젝트를 만들어 보겠다고 호기롭게 시작했습니다.
JPA로 프로젝트를 시작하려니 막상 뭐부터 시작해야할지 너무나 어려웠습니다.
일단 가장 기본적인 것부터 하기 위해서 프로젝트부터 생성을 했습니다.
요즘은 Spring Boot를 대부분 사용하기 때문에 https://start.spring.io/ 에서 아주 쉽고 빠르게 프로젝트를 만들어 올 수 있습니다.
Dependency도 편하게 추가하고 초기 프로젝트 셋팅에 큰 시간을 들이지 않을 수 있습니다. 이렇게 만든 프로젝트는 application.properties를 가지고 있는데, 저는 yml 파일 셋팅이 더 익숙하기 때문에 yml 파일을 사용했습니다.
build.gradle의 코드는 아래와 같습니다.
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.7'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}
group = 'jpaboard'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.hibernate.validator:hibernate-validator'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation"org.springframework.boot:spring-boot-starter-validation"
implementation 'mysql:mysql-connector-java'
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.7.1'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation("org.junit.vintage:junit-vintage-engine") {
exclude group: "org.hamcrest", module: "hamcrest-core"
}
}
tasks.named('test') {
useJUnitPlatform()
}
application.yml의 코드는 아래와 같습니다.
server:
port: 7070
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/JPA_BOARD?serverTimezone=Asia/Seoul&useSSL=false&allowPublicKeyRetrieval=true
username: 유저 ID(대부분 건들지 않으면 root로 되어 있습니다.)
password: 유저 PWD
jpa:
database: mysql
database-platform: org.hibernate.dialect.MySQL8Dialect
open-in-view: false
hibernate:
ddl-auto: create
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
use-new-id-generator-mappings: false
show-sql: true
properties:
hibernate.enable_lazy_load_no_trans: true
hibernate.format_sql: true
http:
encoding:
charset: UTF-8
enabled: true
force: true
저는 8080 & 80 포트가 사용중이므로, 7070 포트를 따로 지정해서 사용하려고 옵션을 주었고 따로 포트를 지정하지 않으면 default가 8080입니다.
hibernate setting 같은 경우에는 기존 강의에서 H2 Database 기반으로 강의를 들었으나, 제가 자주 사용하고 업무에도 쓰고 있는 MySQL을 사용하기 위해서 MySQL을 사용하였고, 현재 개발 단계라서 ddl-auto를 create로 설정해주었습니다.
이제 JPA 활용편을 듣고 있기 때문에 저는 JpaRepository에 대해서 잘 알지 못합니다. 그래서 처음에 JPA를 평소에 사용하던 대로, EntityManager를 주입받아서 활용하는 코드를 작성했었습니다.
하지만 복잡하지 않은 게시판 프로젝트 정도라면 CRUD를 JpaRepository를 상속받아 이용하는 것으로 결정하였기에 이번 프로젝트는 JpaRepository를 사용하기로 결정했습니다.
JpaRepository에 대해서는 추후에 자세히 포스팅을 통해 다루도록 하겠습니다.
JpaRepository를 이용해서 회원 CRUD를 작성해 보았습니다. UserRepository라는 이름의 interface를 만들고 JpaRespository가 가지고 있는 메서드들을 오버라이딩해서 사용하였습니다.
// UserRepository
public interface UserRepository extends JpaRepository<User, Long> {
/*
* 회원 가입
* @param User
* @return userNo
* */
public User save(User user);
/*
* 회원 한명 조회
* @param userNo
* @return User
* */
public Optional<User> findById(Long id);
/*
* 회원 이름으로 조회
* @param userName
* @return User
* */
public User findByName(String name);
/*
* 회원 한명 삭제
* @param userNo
* */
public void delete(User user);
JpaRepository는 상속을 받아 사용할 수 있는데요. JpaRepository<Entity 클래스, Primary Key의 타입>을 적어주시면 됩니다. 저는 User 클래스(엔티티)의 PK값이 Long 타입이라서 저렇게 선언해주었습니다.
회원 한명 조회를 보시면 Optional<User>로 한번 감싸져서(Wrapping) 반환받는 것을 보실 수 있는데요.
회원을 조회했을때 엔티티가 바로 반환되면 null을 다뤄야 하는 상황이 생길 수 있으므로, Optional로 한번 감싸서 안에 들어있는 객체가 없을경우 예외를 던지도록 Service 클래스에서 작성했습니다.
관련된 회원 조회 코드는 아래와 같습니다.
// 회원 조회 코드 - UserService(findOneUser)
/*
* 회원 한명 조회
* @param userNo
* @return User
* */
public User findOneUser(Long userNo) {
Optional<User> findUser = userRepository.findById(userNo);
if (findUser.isEmpty()) {
throw new IllegalStateException("조회하려는 회원은 존재하지 않습니다.");
} else {
return findUser.get();
}
}
PK 값으로 User 한명을 찾아와서 Optional에 담아서 반환받은 뒤에, isEmpty( ) 메서드를 이용해 안에 객체가 있는지 확인하고, 없다면 예외를 던지고 있다면 get( )을 사용해서 User를 반환하게 작성하였습니다.
Null을 직접 다루는 것은 NPE를 발생하게 할 수 있고 여러모로 좋지 않기 때문에 이렇게 작성했습니다.
Update를 살펴보면, UserRepository에 Update에 관한 메서드가 정의되어 있지 않습니다.
그 이유는 JPA의 Dirty Checking을 사용해서 Update Query를 자동으로 실행되게 할 것이기 때문에 따로 만들지 않았습니다.
Update는 Service 클래스에 작성했으며, 코드는 아래와 같습니다.
// 회원 업데이트 코드 - UserService(modifyUser)
/*
* 회원 한명 업데이트
* @param User
* @return userNo
* */
@Transactional
public User modifyUser(User user) {
User modifyUser = userRepository.findById(user.getNo()).get();
modifyUser.setEmail(user.getEmail());
modifyUser.setName(user.getName());
modifyUser.setPwd(user.getPwd());
modifyUser.setModId(user.getModId());
return modifyUser;
}
@Transactional이 붙어있는 이유는, Service 클래스에 @Transactional을 통해 readOnly를 true로 주었기 때문입니다. Update나 Insert,Delete의 경우에는 쓰기 작업이므로 @Transactional을 붙여줘서 readOnly를 true로 만들어줘야 합니다.
🚀 @Transactional의 readOnly 기본 값은 false 입니다.
특정 유저의 변경될 정보를 받아 저장 되어있는 User 객체를 파라미터로 받아 옵니다.
해당 User의 No값을 이용해 User 한명을 조회해 옵니다.
같은 User 객체인데 다시 User를 조회하는 이유는 Dirty-Checking 기능을 사용하기 위해서 입니다.
파라미터로 넘어오는 User는 영속 상태가 아닙니다. 즉 영속성 컨텍스트와 아무런 관련이 없는 상태이기 때문에 파라미터로 넘어오는 User의 내부 값을 변경해도 Update 쿼리는 나가지 않습니다.
따라서 해당 User가 가지고 있는 PK값을 이용해 다시 User를 조회해오면 현재 영속성 컨텍스트에 없기 때문에 DB에서 직접 조회를 해와서 1차 캐시에 등록을 합니다.
영속성 컨텍스트에는 1차 캐시에 User도 등록이 되지만, 조회해온 즉시 그 때의 상태를 SNAP-SHOT에서 관리를 합니다.
SNAP-SHOT에 있는 User와 현재 User의 필드 값들을 비교하고 변경된 부분이 있다면 알아서 Update 쿼리를 실행해 주는 것을 Dirty-Checking 이라고 합니다.
이제 시작이지만 셋팅과 기본적인 CRUD를 만들어 놓으니 그래도 마음이 편안해지네요~
다음 포스팅부터는 Test Code를 작성하고 Thymeleaf를 이용한 SSR(Server-Side-Rendering)등 본격적인 개발을 해보도록 하겠습니다. 2023 새해 복 많이 받으시고 올해도 즐거운 개발 생활 되세요!