실습의 날입니다.
이번엔 실습에 시간을 좀 투자해야 해서 일부러 진도를 많이 안 나가고, 매우 단순할거라 예상했던 Memory CRUD DB 만을 실습해봤습니다. 가능하다면 JPA (Java Persistence API, 자바답게 이런 거 하나하나 다 표준임) 을 써보고싶었는데, 아무래도 10분내에 날름 해치우고 쓰기에는 무리가 있었습니다. C# 계열의 "EF Core" 라는 ORM 을 배워본 적이 있어서 쉬울 줄 알았는데 자바답게 표준때문에 좀 더 깊게 배워야 쓸 수 있을 거 같이 생겼더라구요. 당연히 Hibernate랑 SQLite 를 썼겠지만...
(*참고: 여기있는 코드들은 강의와는 좀 다릅니다. 강의를 한 번 쭉 본 뒤, 가급적이면 안 보고 직접 짜보는 방법을 썼거든요.)
MemoryRepo
별 거 없습니다. 그냥 Generic 을 써서 Repo 와 Entity 를 만들어냅니다.
MemoryRepo - API가 이상하다
눈에 띄는 점 - API가 3개입니다. JPA를 흉내낸 API라고 하시는데, findById, findAll, save, delete 정도가 다입니다. CRUD 중 C와 U가 모두 save 에 있다는 점이 특이점입니다. 리포지터리 패턴을 직접 써본적이 없고 눈으로만 공부해봤어서 잘 몰랐는데 원래 이런걸까요?
Repository 는 단 한 종류의 엔티티만을 CRUD 하도록 설계되었습니다. 바로 고민없이 이렇게 짜시는 걸로 봐서 이게 관습인 모양인데... 도메인 영역의 엔티티를 저장하고 불러오는 것을 추상화하려는 목적이니까, Unit of Work 입장에서 만약 Aggregate 가 대상이 된다면 어떻게 만들어지려나요? 이 부분은 저에게 아직 미지수입니다.
package com.example.matzip.memdb;
import java.util.List;
import java.util.Optional;
public interface IMemRepo<TEnt extends BaseMemEntity> {
Optional<TEnt> findEnt(int id); // R
List<TEnt> findAll(); // R
TEnt save(TEnt ent); // C, U
void delete(int id); // D
}
MemoryRepo - Java를 써보자
아무래도 학습 과정 중 한 번은 Java를 써봐야 할 거 같고, 강의에 stream() API 가 나와서 써보자는 생각으로 써봤습니다. 떵바도 11버전 이후는 나름 괜찮네요! var 도 되고 말이죠.
이거 API가 재밌습니다. ArrayList<T> 에 stream() 을 호출하면 java.util.stream.Stream<E> 로 변하는 거까진 좋았어요. 그런데 first(조건) 이 없습니다. findFirst() 에는 조건을 넣을 수가 없어요. 알고보니 앞에서 filter() 로 거른 뒤 뒤에서 findFirst() 로 마무리하면 되는거였습니다. 약간 C#의 LINQ 같은 lazy evaluation 개념인가보네요.
(아니 그래서 처음 보는 API인데도 살짝 헤맨 거 빼고는 스무스하게 짜버렸다니까... 솔...직히 문서조차 안 읽어봐도 될 거 같음.)
public abstract class BaseMemRepo<TEnt extends BaseMemEntity> implements IMemRepo<TEnt> {
ArrayList<TEnt> items = new ArrayList<>();
int lastAutoGenId = 0;
@Override
public Optional<TEnt> findEnt(int id) {
return items.stream().filter(it -> it.getId() == id).findFirst();
}
...길어서 안 보여줄거야...
@Override
public void delete(int id) {
var entResult = this.findEnt(id);
entResult.ifPresent(it -> items.remove(it));
}
}
아 그리고 너희집 Optional 쩔더라 (이미 다른집(rs/c++)에서 맛보고 와서 여기에도 있는거에 그저 놀랐을 뿐)
특히 ifPresent(it -> action) 이 재밌었습니다.
근데 가만 생각해보니 이건 타입 시스템에서 애초에 null 여부를 지원하고 question 연산자가 있으면 불필요한 거 아니에요? 역시 떵바네
@Override
public void delete(int id) {
var entResult = this.findEnt(id);
entResult.ifPresent(it -> items.remove(it));
}
MemoryRepo - 엔티티와 Lombok 어노테이션
오히려 Lombok 어노테이션에서 헤맸습니다. 처음에 암 생각없이 @RequiredArgsConstructor 를 넣었는데 이게 알고보니 @NonNull 로 지정된 필드만 넣는 생성자더라구요. 그래서 kotlin 쪽에서 생성자 두 개 있다고 아주 ㅋㅋㅋㅋ
이게 안쓰고 바로 kotlin 으로 넘어가버릇을 하니까... @Data 까지 까먹었다니까요? kotlin은 그냥 data class 해버리면 되니까...
@NoArgsConstructor
@AllArgsConstructor
@Data
public abstract class BaseMemEntity {
private int id;
}
MemoryRepo - 여담 - 네이밍
커리어 시작이 C++/C# 이었어서, Java 식으로 인터페이스나 추상클래스에 아무런 이름도 안 붙어있는 거 너무 싫습니다.
- 인터페이스: 강의에서는 뒤에 "Ifs"가 붙었는데 이럴거면 앞에 I를 붙여주세요.
- 추상 클래스: 추상 클래스는 앞에 Abstract 혹은 Base 를 붙이면 알아보기 쉽습니다. 짧으니까 Base.
맛집 장소
이제 베이스클래스는 다 짰으니까 강의에서 했던거처럼 데이터베이스(MemDB)에 들어갈 실제 클래스를 상속받아서 만들어야 합니다. 근데 그러려니까 그냥 kotlin 쓰고싶더라구요. 스와아아아압!!!! 코틀린 추가!!!!!!!
맛집 장소 - kotlin 으로
하이라이트는 data class입니다. PlaceEntity 는 Java에서 만든 클래스인 BaseMemEntity 를 상속받아야 하거든요.
문제는 BaseMemEntity 에 필드가 있다는 겁니다. id 말이에요.
@NoArgsConstructor
@AllArgsConstructor
@Data
public abstract class BaseMemEntity {
private int id;
}
kotlin 으로 BaseMemEntity 를 상속받으려고 했고, 생성자에 id도 넣고싶었습니다. 하지만 data class 생성자의 모든 필드는 뭐다? val 이나 var 필드어야 한다~ 그래서 상속받은 클래스의 부모도 kotlin 이면 부모쪽에서 abstract 필드로 정의를 해주는데, 이건 부모가 Java니까 abstract field 선언이 안 되잖아요...
data class PlaceEntity(
// 마 니 부모클래스에도 ID 있는데 또 정의할래?
// "우발적 재정의: 다음 선언에 동일한 JVM 시그니처가 있습니다(getId()I)."
var id: Int,
var title: String,
var visited: Boolean
): BaseMemEntity()
data class PlaceEntity(
// 뭐라꼬? 할 게 있나?
// vvvvvvvv "'id' overrides nothing"
override var id: Int,
var title: String,
var visited: Boolean
): BaseMemEntity()
그럼 뭐다? 빠른 포기다
data class PlaceEntity(
var title: String,
var visited: Boolean
): BaseMemEntity()
테스트
강의에서는 Spring / JUnit 을 활용한 MemDB의 CRUD 테스트 를 간단하게 해봤습니다. 되는거도 확인했겠다, 짜다가 귀찮아서 그만둠
당장 Place 클래스에도 원래대로라면 강의처럼 온갖 필드가 다 들어있어야 할텐데 거기까지는 굳이 안 했습니다. 강의에서 필드가 한 8~9개 생기는 걸 보고 아 실제 DB는 저래되겠구나 싶드라구여 ㅇㅇ... 필드 많은거 너저분해... 현업은 어쩔수없지...
@SpringBootTest
class PlaceRepoTests {
private fun createEnt() = PlaceEntity(
"맛있는 맛집",
visited = true
)
private fun createPredefinedState(): PlaceRepo {
val ent1 = createEnt() // 1
val ent2 = createEnt().copy("어우야 맛집", false)
val repo = PlaceRepo()
repo.save(ent1)
repo.save(ent2)
return repo
}
@Test
fun saveNonExisting_correctId() {
// prepare
val repo = createPredefinedState()
// run
val ent = createEnt()
ent.id = 0
val savedEnt = repo.save(ent)
// assert
Assertions.assertEquals(3, savedEnt.id)
Assertions.assertEquals(3, repo.findAll().size)
}
// 함수 리스트는 많이 만들었는데 내용을 안 채움. 커버리지 띄울것도 아니고...
@Test fun saveExisting_correctIdWithSize() {}
@Test fun deleteExisting() {}
@Test fun deleteNonExisting() {}
@Test fun findExisting_found() {}
@Test fun findNonExisting_empty() {}
}
본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.
패스트캠퍼스: https://bit.ly/37BpXiC
#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #한번에끝내는JavaSpring웹개발마스터초격차패키지Online
예상대로 시간을 많이 써서 진도를 적게 나간 게 적절한 판단이었다...
아 근데 요즘 신기한 경험 하고 있어요. 회사에서 다루고 있는 데모 프로젝트가 Spring Boot WebMVC + WebSocket 이어서 이번 챌린지로 계속 배우고 있는 거들 덕분에 코드가 조금씩 더 보임 ㅋㅋ;
'FastCampus - 한번에 끝내는 Java|Spring 웹 개발 > 03 스프링 입문' 카테고리의 다른 글
스프링 Ch 9 Swagger/OpenAPI - 패스트캠퍼스 챌린지 15일차 (0) | 2022.02.07 |
---|---|
스프링 Ch 8-2~3 Spring 테스트 - 패스트캠퍼스 챌린지 14일차 (0) | 2022.02.06 |
스프링 Ch 7-3 네이버 검색 연동 / JUnit - 패스트캠퍼스 챌린지 13일차 (0) | 2022.02.05 |
스프링 Ch 7 Spring as client - 패스트캠퍼스 챌린지 12일차 (0) | 2022.02.04 |
스프링 Ch 6-5 Filter / Interceptor - 패스트캠퍼스 챌린지 11일차 (0) | 2022.02.03 |