var 프로퍼티가 public으로 열려있어 setter를 쓸 수 있지만 setter 대신 좋은 이름의 함수(=updateName)을 사용하는 것이 훨씬 clean하다. 하지만 name에 대한 setter는 public이기 때문에 유저 이름 업데이트 기능에서 sette r를 사용할'수도' 있다. (=updateName=setName) 따라서 setter만 private하게 만드는 것이 좋다.
첫 번째 방법(backing property 사용)
내부에서는 _name(언더바name)을 사용해서 이름 값을 바꿀 수 있고 외부에서는 불변이 (val) name에 접근해서 Get할 수 있다.
두 번째 방법(custom setter 이용하기)
위 두 방법 모두 property가 많아질수록 번거롭다. 따라서 setter를 열어는 두지만 사용하지 않는 방법을 선호 -> 팀 컨벤션을 잘 맞추면 된다.
생성자 안의 프로퍼티. 클래스 body 안의 프로퍼티
꼭 primary constructor 안에 모든 프로퍼티를 넣어야 할까?
body에 만들어도 잘 동작한다.
추천
모든 프로퍼티를 생성자에 넣거나
프로퍼티를 생성자 혹은 클래스 body 안에 구분해서 넣을 때 명확한 기준이 있거나
JPA와 data class
Entity는 data class를 피하는 것이 좋다.
equals, hashCode, toString 모두 JPA Entity와는 100% 어울리지 않는 메서드
위의 경우에서
User의 quals가 UserLoanHistory의 equals를 부른다.
UserLoanHisoty의 equals가 User의 quals를 부른다.
TIP
Entity가 생성되는 로직을 찾고 싶으면 constructor 지시어를 명시적(임의로)으로 작성하고 추적하자!
단순 Book으로 눌러봤을 때는 모든 class가 나오고 constructor를 임의로 작성하고 내부를 눌러보면 딱 '생성'한 부분만 추적가능하다.
결론 관계형 데이터 베이스에선 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다. 그렇지만 객체에서는 가능하다.
Member 쪽에서 products쪽과 ManyToMany로 연결해준다.
Product 쪽에서 JoinTable(Member_product)을 선언하여 링크테이블을 생성해주는 어노테이션을 단다.
자동으로 1:n, n:1 관계를 만드는 것과 같은 원리이다.
오! 편리하다. 생각하고 사용하면 안된다. - 연결 테이블이 단순히 연결만 하고 끝나지 않는다. (무슨말이지? 단순히 연결만 하고 끝나지 않고 추가 데이터가 들어간다. 별에별게 들어간다. 또한 중간 테이블이 예상할 수가 없다.) - 주문시간, 수량 같은 데이터가 추가로 들어올 수 있다. (Member_Product에 들어감)
리팩토링
연결 테이블용 엔티티 추가 (연결 테이블을 엔티티로 승격!)
Member : 링크 테이블과 연결
Product : 링크 테이블과 연결
MemberProduct : 링크 테이블 추가
count, price, OrderDateTime을 넣을 수 있음
결론 양방향으로 여러개를 가질 수 있을 때 고민해야된다. 주인 한명에 강아지 2개 반대로 강아지 한마리에 주인이 여러명일 경우 우리가 그렇네! 그럴 경우 링크테이블을 걸어야한다.
도메인 모델과 테이블 설계
회원 <-> 주문 : 회원은 주문을 여러 건 할 수 있기 때문에 1:n의 관계이다.
주문<-> 배송 : 주문과 배송은 1:1 의 관계이다.
주문<->상품 : 주문과 상품은 n:n의 관계이다. 왜냐하면 한 번 고객이 한 번 주문 할때 여러 상품을 선택할 수 있기 때문이다. 이런 다대다 관계는 관계형 데이터베이스는 물론이고 엔티티에서도 거의 사용하지 않는다. 따라서 주문상품을 추가하여 주문 <-> 주문상품 <->상품(물품)으로 1:n, n:1로 풀어냈다.
엔티티 설계
회원(Member) : 이름과 임베디드 타입인 주소(Address), 그리고 주문(orders)리스트를 가진다.
주문(Order) : 한 번 주문시 여러 상품을 주문할 수 있으므로 주문과 주문상품(OrderItem)을 일대다 관계다.
주문상품(OrderItem) : 주문한 상품정보와 주문 금액(OrderPrice), 주문수량(count) 정보를 가지고 있다.
상품(Item) :이름, 가격, 재고수량(stockQuantity)을 가지고 있다. 상품을 주문하면 재고수량이 줄어든다.
plugins {
id 'org.springframework.boot' version '2.6.8'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
id 'org.jetbrains.kotlin.jvm' version '1.6.21'
id 'org.jetbrains.kotlin.plugin.jpa' version '1.6.21'
id 'org.jetbrains.kotlin.plugin.spring' version '1.6.21'
id 'org.jetbrains.kotlin.kapt' version '1.6.21'
}
group = 'com.group'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' // 코틀린을 사용하기 위한 의존성 추가
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.6.21' // 코틀린을 사용하기 위한 의존성 추가
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.13.3' // 코틀린을 사용하기 위한 의존성 추가
implementation 'org.junit.jupiter:junit-jupiter:5.8.1' // 코틀린을 사용하기 위한 의존성 추가
implementation 'com.querydsl:querydsl-jpa:5.0.0' //querydsl 의존성
kapt("com.querydsl:querydsl-apt:5.0.0:jpa")
kapt("org.springframework.boot:spring-boot-configuration-processor")
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
/**
* 코틀린에 필요한 compile 옵션 추가
*/
compileKotlin {
kotlinOptions {
jvmTarget = "11"
}
}
compileTestKotlin {
kotlinOptions {
jvmTarget = "11"
}
}
package com.group.libraryapp.calculator
import java.lang.IllegalArgumentException
data class Calculator (
private var number: Int
){
fun add(operand: Int){
this.number += operand
}
fun minus(operand: Int){
this.number -= operand
}
fun multiply(operand: Int){
this.number *= operand
}
fun divide(operand: Int){
if(operand == 0){
throw IllegalArgumentException("0으로 못나눔")
}
this.number /= operand
}
}
메인을 만들어 테스트를 진행한다.
package com.group.libraryapp.calculator
import java.util.Calendar
/**
* main 생성
*/
fun main(){
val calculatorTest = CalculatorTest()
calculatorTest.addTest();
}
class CalculatorTest {
fun addTest(){
val calculator = Calculator(5)
calculator.add(3)
val expectedCalculator = Calculator(8)
if(calculator != expectedCalculator){
throw IllegalStateException()
}
}
}
data를 지우고 number를 public으로 하거나 get을 열어주어 test에서도 접근가능하도록한다.
코드 컨벤션 _number
package com.group.libraryapp.calculator
import java.lang.IllegalArgumentException
class Calculator (
//private var number: Int
//var number: Int // setter를 연 상태
private var _number: Int
){
/**
* getter를 연상태
*/
val number: Int
get() = this._number
fun add(operand: Int){
this._number += operand
}
fun minus(operand: Int){
this._number -= operand
}
fun multiply(operand: Int){
this._number *= operand
}
fun divide(operand: Int){
if(operand == 0){
throw IllegalArgumentException("0으로 못나눔")
}
this._number /= operand
}
}
test쪽에서 바로 get 가능하다.
package com.group.libraryapp.calculator
import java.util.Calendar
/**
* main 생성
*/
fun main(){
val calculatorTest = CalculatorTest()
calculatorTest.addTest();
}
class CalculatorTest {
fun addTest(){
// given
val calculator = Calculator(5)
//when
calculator.add(3)
/*
val expectedCalculator = Calculator(8)
if(calculator != expectedCalculator){
throw IllegalStateException()
} */
//then
if(calculator.number != 8 ){
throw IllegalStateException()
}
}
}
Junit5 사용법과 테스트 코드 리팩토링
Junit5에서 사용되는 5가지 어노테이션
@Test : 테스트
@BeforeEach : 각 테스트 메소드가 수행되기 전에 실행되는 메소드를 지정한다.
@AfterEach : 각 테스트가 수행된 후에 실행되는 메소드를 지정한다.
@BeforeAll : 모든 테스트를 수행하기 전에 최초 1회 수행되는 메소드를 지정한다.
package com.group.libraryapp.calculator
import org.junit.jupiter.api.*
class JunitTest {
companion object {
@BeforeAll
@JvmStatic
fun beforeAll() {
println("모든 테스트 시작 전")
}
@AfterAll
@JvmStatic
fun afterAll() {
println("모든 테스트 종료 후")
}
}
@BeforeEach
fun beforeEach() {
println("각 테스트 시작 전")
}
@AfterEach
fun afterEach() {
println("각 테스트 종료 후")
}
@Test
fun test1() {
println("테스트 1")
}
@Test
fun test2() {
println("테스트 2")
}
}
결과
계산기에 적용하기
assertThat Imort 하기
AssertProvider 선택하기
테스트 코드 작성하기
@Test
fun addTest(){
// given
val calculator = Calculator(5)
// when
calculator.add(3)
// then
assertThat(calculator.number).isEqualTo(7);
}
테스트 코드 결과
추가로 사용하는 단언문
isTrue/isFalse : true/false 검증
// then
val isNew = true
assertThat(isNew).isTrue();
assertThat(isNew).isFalse();
hasSize(n) : size 검증 (주로 list의 갯수를 확인)
extracting/containsExactlyInAnyOrder : 주어진 컬렉션 안의 Item 들에서 name 이라는 프로퍼티를 추출한 후, 그 값이 A와 B인지를 검증한다.(AnyOrder : 이 때 순서는 중요하지 않다)
assertThrows : funtion1 함수를 실행했을 때 liigalArgumentException이 나오는지 검증
만약 나온다면 message로 던져주는 메서드
Junit5으로 Spring Boot 테스트 하기
Controller - bean 관리 대상이므로 @SpringBootTest로 진행
Service - bean 관리 대상이므로 @SpringBootTest로 진행
Repository - bean 관리 대상이므로 @SpringBootTest로 진행
Domain - bean 관리 대상이 아니므로 @Test 진행
어떤 계층을 테스트 해야 할까?
보통은 Service 계층을 테스트한다. 보통 A를 보냈을 때 B가 잘 나오는지, 원하는 로직을 잘 수행하는지 검증할 수 있기 때문이다.
@Autowired 해주기
class UserServiceTest @Autowired constructor(
private val userRepository: UserRepository
,private val userService: UserService
) {