게시글
블로그
전체 82025. 06. 08.
1
인프런 워밍업 클럽 4기 BE 스터디 2주차
💻 강의Readable Code: 읽기 좋은 코드를 작성하는 사고법 📚 학습주석주석이 많다는 것은 비즈니스의 요구사항을 코드에 잘 못 녹였다는 것코드를 설명하는 주석은 코드가 아닌 주석에 의존한 것주석에 의존하여 코드를 작성하면 적절하지 않은 추상화 레벨을 갖게 돼 낮은 품질의 코드가 만들어질 수 있다언제 주석을 사용해야 할까?후대에 전해야 할 의사 결정의 히스토리를 코드로 표현할 수 없을 때 상세한 설명이 필요하기 때문에 이때 주석이 필요하다 주석 작성주석을 작성할 때, 자주 변하는 정보는 지양한다코드가 변경되면 주석도 같이 업데이트 해야 하기 때문에 주석이 없는 코드보다 부정확한 주석이 달린 코드가 더 치명적이다좋은 주석최대한 코드에 녹여내고, 코드로 표현 못하는 정보를 전달해야 할 때 주석을 사용한다회고지금은 컨트롤러에만 주석을 남기거나 학습이 중점인 개인 프로젝트에만 주석을 남기는 편이다하지만 예전에 팀 프로젝트를 할 때는 코드마다 주석을 남겼고, 클린 코드에 관심을 가지게 되면서 코드를 최대한 간결하게 작성하려고 하는데, 주석을 남기는 게 맞는지 의문이 생겨 부트캠프 튜터님들과 알고 지내는 주니어 개발자 분들에게 질문을 드린 적이 있다튜터님들이 생각하시는 클린 코드는 학습 내용처럼 주석 없이 최대한 코드에 녹여내는 것이 클린 코드라고 하셨고, 주니어 개발자 사이에서는 의견이 각각 달랐다아무래도 주위에 미들이나 시니어 개발자 보다는 같은 주니어 개발자가 대부분이기 때문에 의문점이 생기면 학습을 통해 배우거나 나보다 경험이 많은 개발자 선배들에게 물어보는 것이 최고인 것 같다튜터님들께 질문을 드려 피드백을 받았다고 해도 주변 주니어 개발자 분들과 마주칠 일이 많다 보니 주석에 대한 혼란이 조금은 남아있었는데 학습을 통해 주석에 대한 의문점이 완전히 풀렸다 상황에 따라 다르겠지만, 내가 배운 내용에 대해 자신감을 가져야겠다아는 것이 힘이다!
백엔드
2025. 06. 01.
1
인프런 워밍업 클럽 4기 BE 스터디 1주차
💻 강의Readable Code: 읽기 좋은 코드를 작성하는 사고법 📚 학습Early returnEarly return으로 else 사용 지양하기표현하고자 하는 것이 명확하다면 else 없이 바로 반환하는 방향으로 리팩토링 진행하기부정어부정 연산자는 가독성을 낮춤부정어구를 쓰지 않아도 되는 상황인지 확인하기부정어구를 사용할 수 밖에 없는 상황이라면 부정어구로 메서드명 구성하기doesNotisNot 회고항상 코딩을 하면서 클린 코드를 추구하기 때문에 if 문을 사용할 때, 가독성을 높이기 위해 else 사용을 지양해왔다1주차 학습을 하면서 'Early return' 용어를 처음 접했고, 용어를 모른 채 코드의 가독성을 높이기 위해 else 사용을 지양해 온 것에 대해 좋은 방향으로 나아가고 있다는 것을 깨달았다부정 연산자가 코드의 가독성을 낮출 수 있다는 것은 인지하지 못했다지금이라도 부정어를 대하는 자세에 대해 배우게 돼 다행이다앞으로는 부정어구를 쓰지 않아도 되는 상황인지 확인하고, 최대한 부정 연산자의 사용을 자제해야겠다 🎯 미션Day 2 미션Day 4 미션회고미션을 진행하면서 추상과 구체의 개념에 대해 다시 배우게 됐고, 1주차에 배운 내용을 기반으로 Day 4 미션을 통해 막상 리팩토링을 해보니 어려웠다처음에는 어떤 부분부터 어떻게 리팩토링을 해야 할지 막막했지만, 처음에 배운 추상과 구체가 떠올라 추상화부터 하였다추상적으로 하는 것만으로도 코드가 깔끔해 보였고, 추상화의 중요성에 대해 깨닫게 되었다
백엔드
2025. 05. 29.
0
인프런 워밍업 클럽 4기 BE 스터디 Day 4 미션
Readable Code: 읽기 좋은 코드를 작성하는 사고법 🎯 Day 4 미션 ①[섹션 3. 논리, 사고의 흐름]에서 이야기하는 내용을 중심으로 읽기 좋은 코드로 리팩토링 하기추상화 하기Early return 적용부정어구를 사용해야 하는지 고민하고, 사용해야 한다면 부정어구로 메서드명 지정public boolean validateOrder(Order order) { if (hasNoOrderItem(order)) { log.info("주문 항목이 없습니다."); return false; } if (hasTotalPrice(order)) { if (order.hasCustomerInfo()) { return true; } return false; } if (hasNoTotalPrice(order)) { log.info("올바르지 않은 총 가격입니다."); return false; } return true; } private boolean hasNoTotalPrice(Order order) { return order.getTotalPrice() 0; } private boolean hasNoOrderItem(Order order) { return order.getItems().size() == 0; } 🎯 Day 4 미션 ②SOLID에 대하여 자신만의 언어로 정리하기SRP책임을 분리하자 OCP추상화와 다형성을 활용해 기존 코드 변경 없이, 기능을 확장할 수 있어야 한다 LSP자식 클래스는 부모 클래스의 행위를 변경하지 않는다부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 치환할 수 있어야 한다 ISP기능 단위로 인터페이스를 나누자 DIP추상화(인터페이스)에 의존하자
백엔드
2025. 05. 28.
0
인프런 워밍업 클럽 4기 BE 스터디 Day 2 미션
Readable Code: 읽기 좋은 코드를 작성하는 사고법 🎯 Day 2 미션 : 추상과 구체의 예시추상밥을 먹었다 구체젓가락으로 반찬을 집어 밥과 함께 입에 넣는다입 안으로 들어간 음식물은 저작 운동을 통해 잘게 부서진다식도를 통해 위로 내려가고, 위는 음식물을 분해한다
백엔드
2025. 03. 24.
0
인프런 워밍업 클럽 3기 BE 스터디 4주차
💻 강의입문자를 위한 Spring Boot with Kotlin - 나만의 포트폴리오 사이트 만들기 📚 학습@Transactional(readOnly = true)읽기 전용 트랜잭션으로 설정하면 데이터 변경이 일어나지 않기 때문에 스냅샷을 저장하는 동작을 생략해 좀 더 성능을 개선할 수 있다readOnly 참고Service 테스트@ExtendWith : 테스트 확장을 지원하며 JUniit5와 Mockito를 연동해 테스트를 진행할 경우에는 MockitoExtension.class를 사용@InjectMocks : Mockito에서 테스트 대상이 되는 클래스에 인스턴스를 주입하기 위해 사용@Mock : Mockito에서 Mock 객체를 생성할 때 사용하며 실제로 메서드는 갖고 있지만 내부 구현이 없는 상태참고 Repository 테스트를 했을 때와는 다르게 Service 테스트를 할 때는 Mockito를 사용한다@ExtendWith(MockitoExtension::class) class PresentationServiceTest { @InjectMocks lateinit var presentationService: PresentationService @Mock lateinit var presentationRepository: PresentationRepository }Service 테스트 코드 해석given일단 홀수이면 isActive = false, 짝수이면 true로 설정한다필터링을 해 isActive = true인 것만 남긴다presentationRepository.getActiveIntroductions()를 실행하면 필터링 한 데이터를 반환하도록 한다["2", "4", "6"]when테스트 대상 메서드를 실행한다thenDATA_SIZE = 7이므로 DATA_SIZE / 2 = 3.5이지만 정수 연산에서는 3이 반환 된다필터링 한 데이터 수가 일치하면 첫 번째 검증은 통과이다content 값을 정수로 변환하고, isEven()을 사용해 짝수인지 검증한다짝수이면 두 번째 검증도 통과이다@Repository class PresentationRepository( ...) { ... fun getActiveIntroductions(): List { return introductionRepository.findAllByIsActive(true) }... @Test fun testGetIntroductions() { // given val introductions = mutableListOf() for (i in 1..DATA_SIZE) { // 1, 3, 5, 7 -> false / 2, 4, 6 -> true val introduction = Introduction(content = "${i}", isActive = i % 2 == 0) introductions.add(introduction) } val activeIntroductions = introductions.filter { introduction -> introduction.isActive } Mockito.`when`(presentationRepository.getActiveIntroductions()) .thenReturn(activeIntroductions) // when val introductionDTOs = presentationService.getIntroductions() // then assertThat(introductionDTOs).hasSize(DATA_SIZE / 2) for (introductionDTO in introductionDTOs) { assertThat(introductionDTO.content.toInt()).isEven() } } Controller 테스트@SpringBootTest : 실제 애플리케이션과 유사한 환경을 구성하여 테스트 가능@AutoConfigureMockMVC : Spring MVC를 모의로 테스트하는 데 사용@SpringBootTest @AutoConfigureMockMvc @DisplayName("[API 컨트롤러 테스트]") class PresentationApiControllerTest(@Autowired private val mockMvc: MockMvc) { }Thymeleaf 문법th:fragment : 템플릿의 일부를 재사용 가능한 fragment로 정의th:replace : 해당 요소를 다른 요소로 대체할 때 사용 fragment 이름을 navigation으로 지정하고, 해당 경로에 있는 파일의 navigation fragment를 찾아서 대체한다// /templates/presentation/fragments/fragment-navigation.html // /templates/presentation/index.html 🎯 미션 6과 미션 7가상 프로필을 나의 프로필로 바꾸기강의 실습 프로젝트의 데이터 초기화 클래스 내용을 나의 프로필로 바꾼 뒤 커밋커밋 메시지 : [미션6] 가상 프로필을 나의 프로필로 바꾸기미션6 제출 스레드에 깃허브 커밋 링크를 공유프로젝트를 배포한 뒤 브라우저에서 접속미션7 제출 스레드에 도메인 주소를 공유문제이미지 태그를 지정하고 Dockerfile을 Build 하는 과정에서 test 실패 오류가 발생하였다테스트 코드가 문제인 건지 확인하기 위해 TestApplication을 실행해 전체 테스트 코드를 실행시키니 오류는 없었다원인을 찾을 수 없어서 계속 구글링을 해보니 아래 코드가 원인이 될 수도 있다고 한다tasks.withType { useJUnitPlatform() }해결아래 코드를 주석 처리하니 정상적으로 Build가 됐다tasks.withType { useJUnitPlatform() }참고
백엔드
2025. 03. 17.
0
인프런 워밍업 클럽 3기 BE 스터디 3주차
💻 강의입문자를 위한 Spring Boot with Kotlin - 나만의 포트폴리오 사이트 만들기 📚 학습Data ClassData Class가 무엇이고, 어떤 용도로 사용되는지 찾아봤다Data Class 참고CustomExceptionJava Spring Project를 할 때 ErrorCode를 enum으로 관리해 CustomException을 사용한 적이 있다미니 프로젝트에 CustomException을 적용하려고 Kotlin으로 변환해보니 막히는 부분이 있었지만, 참고한 자료 덕분에 해결할 수 있었다CustomException 참고 { "timestamp": "2025-03-18T01:46:23.5967528", "status": 404, "message": "사용자를 찾을 수 없습니다." }아쉬운 점이번 주는 몸이 안 좋아서 평일 동안 회복하는 데 집중했고, 주말에는 평일에 못한 미션 4와 미션 5를 제출하였다작년 9월부터 지금까지 부트캠프와 스터디를 하면서 실력 향상에 집중하다 보니 피로가 쌓여 면역력이 떨어졌다😥건강 관리를 못해 결국 이번 주에 강의를 많이 듣지 못한 부분이 아쉽다회고토요일 오전에 수액을 맞고 많이 괜찮아져서 다음 주는 이번 주에 못한 만큼 열심히 해야겠다다음 주는 강의도 듣고, 미니 프로젝트 기능 보완, 인증/인가 구현, 예외 처리를 해야겠다 🎯 미션 4와 미션 5조회 REST API 만들기조회 API를 개발한 뒤 테스트 코드를 작성테스트 케이스는 3개 이상, 모든 케이스가 어떤 환경에서도 성공해야 함커밋 메시지 : [미션4] 조회 REST API 만들기미션4 제출 스레드에 깃허브 커밋 링크를 공유삽입, 수정, 삭제 REST API 만들기삽입, 수정, 삭제 API를 개발한 뒤 테스트 코드를 작성테스트 케이스는 API별로 3개 이상, 모든 케이스가 어떤 환경에서도 성공해야 함커밋 메시지 : [미션5] 삽입, 수정, 삭제 REST API 만들기미션5 제출 스레드에 깃허브 커밋 링크를 공유문제테스트 케이스를 3개 이상 작성해야 하는데 어떤 경우로 나뉘어서 작성해야 하는지 잘 모르겠다아직 테스트 케이스를 작성하는 것은 어려워서 일단 Repository에서 사용하는 메서드가 제대로 동작하는지 테스트 코드를 작성해 확인하였다findAll()findById()findDepartmentByCode()findCourseByIdAndStudent()회고findCourseByIdAndStudent() 메서드가 잘 동작하는지 테스트 코드를 작성했을 때 의도한 대로 결과가 나와서 뿌듯했다아직은 간단하게 테스트 코드를 작성할 수 있을 정도이지만, 계속 하다 보면 익숙해질 것 같다... @Test fun testFindCourseByIdAndStudent() { logger.info { "findCourseByIdAndStudent 테스트 시작" } val student = userRepository.findById(1L).get() val course = courseRepository.findCourseByIdAndStudent(1L, student).get() logger.info { "학생 이름: ${student.name}" } logger.info { "수강 과목의 학생 이름: ${course.student.name}" } assertThat(course.student).isEqualTo(student) logger.info { "findCourseByIdAndStudent 테스트 종료" } } }2025-03-23T21:26:12.090+09:00 INFO 5532 --- [ main] c.k.a.d.c.r.CourseRepositoryTest : findCourseByIdAndStudent 테스트 시작 Hibernate: select u1_0.id, u1_0.academic_year, u1_0.code, u1_0.created_at, u1_0.department_id, u1_0.login_id, u1_0.name, u1_0.password, u1_0.role, u1_0.updated_at from users u1_0 where u1_0.id=? Hibernate: select c1_0.id, c1_0.created_at, c1_0.student_id, c1_0.subject_id from course_enrollment c1_0 where c1_0.id=? and c1_0.student_id=? 2025-03-23T21:26:12.163+09:00 INFO 5532 --- [ main] c.k.a.d.c.r.CourseRepositoryTest : 학생 이름: 학생 2025-03-23T21:26:12.164+09:00 INFO 5532 --- [ main] c.k.a.d.c.r.CourseRepositoryTest : 수강 과목의 학생 이름: 학생 2025-03-23T21:26:12.165+09:00 INFO 5532 --- [ main] c.k.a.d.c.r.CourseRepositoryTest : findCourseByIdAndStudent 테스트 종료
백엔드
2025. 03. 14.
0
인프런 워밍업 클럽 3기 BE 스터디 2주차
💻 강의입문자를 위한 Spring Boot with Kotlin - 나만의 포트폴리오 사이트 만들기 📚 학습@Profile프로필이 default일 때만 DataInitializer 클래스를 생성해 빈으로 등록@Component @Profile(value = ["default"]) class DataInitializer { } kotlin-loggingprintln()을 사용하지 않고, log를 사용하고 싶어서 따로 찾아보고 적용했다kotlin-logging 방식을 적용했고, Kakao Pay 기술 블로그가 도움이 됐다최신 버전Kakao Pay 기술 블로그private val logger = KotlinLogging.logger { } @Component @Profile(value = ["default"]) class DataInitializer(...) { @PostConstruct fun initializeData() { logger.info { "테스트 데이터 초기화" } ...2025-03-14T04:50:10.942+09:00 INFO 12988 --- [ main] c.j.portfolio.domain.DataInitializer : 테스트 데이터 초기화Repository 테스트 코드@DataJpaTest : JPA 관련 테스트를 위한 설정을 제공@TestInstance : 테스트 인스턴스의 라이프사이클을 지정 TestInstance 부분이 이해가 잘 안돼서 따로 찾아보니 이해할 수 있었다@TestInstance 참고Fetch JoinJoin을 활용해 한 번에 부모와 자식 데이터를 조회할 수 있지만 OneToMany, ManyToMany 관계의 자식 Entity가 여러 개일 경우, 하나만 조인할 수 있다는 한계가 있다@Query("select e from Experience e join fetch e.details where e.isActive = :isActive") fun findAllByIsActive(isActive: Boolean): List// Fetch Join 적용 전 SIZE = 5 2025-03-16T03:40:41.646+09:00 INFO 2212 --- [ main] c.j.p.d.r.ExperienceRepositoryTest : findAllByIsActive 테스트 시작 Hibernate: select e1_0.id, e1_0.created_date_time, e1_0.description, e1_0.end_month, e1_0.end_year, e1_0.is_active, e1_0.start_month, e1_0.start_year, e1_0.title, e1_0.updated_date_time from experience e1_0 where e1_0.is_active=? 2025-03-16T03:40:41.721+09:00 INFO 2212 --- [ main] c.j.p.d.r.ExperienceRepositoryTest : experiences.size: 5 Hibernate: select d1_0.experience_id, d1_0.id, d1_0.content, d1_0.created_date_time, d1_0.is_active, d1_0.updated_date_time from experience_detail d1_0 where d1_0.experience_id=? 2025-03-16T03:40:41.729+09:00 INFO 2212 --- [ main] c.j.p.d.r.ExperienceRepositoryTest : experience.details.size: 1 Hibernate: select d1_0.experience_id, d1_0.id, d1_0.content, d1_0.created_date_time, d1_0.is_active, d1_0.updated_date_time from experience_detail d1_0 where d1_0.experience_id=? 2025-03-16T03:40:41.731+09:00 INFO 2212 --- [ main] c.j.p.d.r.ExperienceRepositoryTest : experience.details.size: 2 Hibernate: select d1_0.experience_id, d1_0.id, d1_0.content, d1_0.created_date_time, d1_0.is_active, d1_0.updated_date_time from experience_detail d1_0 where d1_0.experience_id=? 2025-03-16T03:40:41.734+09:00 INFO 2212 --- [ main] c.j.p.d.r.ExperienceRepositoryTest : experience.details.size: 3 Hibernate: select d1_0.experience_id, d1_0.id, d1_0.content, d1_0.created_date_time, d1_0.is_active, d1_0.updated_date_time from experience_detail d1_0 where d1_0.experience_id=? 2025-03-16T03:40:41.736+09:00 INFO 2212 --- [ main] c.j.p.d.r.ExperienceRepositoryTest : experience.details.size: 4 Hibernate: select d1_0.experience_id, d1_0.id, d1_0.content, d1_0.created_date_time, d1_0.is_active, d1_0.updated_date_time from experience_detail d1_0 where d1_0.experience_id=? 2025-03-16T03:40:41.741+09:00 INFO 2212 --- [ main] c.j.p.d.r.ExperienceRepositoryTest : experience.details.size: 5 2025-03-16T03:40:41.742+09:00 INFO 2212 --- [ main] c.j.p.d.r.ExperienceRepositoryTest : findAllByIsActive 테스트 종료// Fetch Join 적용 후 SIZE = 5 2025-03-16T03:43:37.319+09:00 INFO 9488 --- [ main] c.j.p.d.r.ExperienceRepositoryTest : findAllByIsActive 테스트 시작 Hibernate: select e1_0.id, e1_0.created_date_time, e1_0.description, d1_0.experience_id, d1_0.id, d1_0.content, d1_0.created_date_time, d1_0.is_active, d1_0.updated_date_time, e1_0.end_month, e1_0.end_year, e1_0.is_active, e1_0.start_month, e1_0.start_year, e1_0.title, e1_0.updated_date_time from experience e1_0 join experience_detail d1_0 on e1_0.id=d1_0.experience_id where e1_0.is_active=? 2025-03-16T03:43:37.369+09:00 INFO 9488 --- [ main] c.j.p.d.r.ExperienceRepositoryTest : experiences.size: 5 2025-03-16T03:43:37.372+09:00 INFO 9488 --- [ main] c.j.p.d.r.ExperienceRepositoryTest : experience.details.size: 1 2025-03-16T03:43:37.374+09:00 INFO 9488 --- [ main] c.j.p.d.r.ExperienceRepositoryTest : experience.details.size: 2 2025-03-16T03:43:37.375+09:00 INFO 9488 --- [ main] c.j.p.d.r.ExperienceRepositoryTest : experience.details.size: 3 2025-03-16T03:43:37.376+09:00 INFO 9488 --- [ main] c.j.p.d.r.ExperienceRepositoryTest : experience.details.size: 4 2025-03-16T03:43:37.376+09:00 INFO 9488 --- [ main] c.j.p.d.r.ExperienceRepositoryTest : experience.details.size: 5 2025-03-16T03:43:37.378+09:00 INFO 9488 --- [ main] c.j.p.d.r.ExperienceRepositoryTest : findAllByIsActive 테스트 종료Batch Fetch SizeIN 절을 사용해 여러 건의 데이터를 한 번에 조회할 수 있지만 한 번에 많은 데이터를 불러오는 것은 애플리케이션이나 데이터베이스에 부담을 줄 수 있기 때문에 적절한 개수 설정이 필요하다// Batch Fetch Size = 10일 경우 ?도 10개 ... project_id in (?,?,?,?,?,?,?,?,?,?) Batch Fetch Size를 적용하기 전에는 detail에 대한 쿼리가 매번 실행됐지만, 적용 후에는 한 번만 실행된다Size는 5인데 Batch Fetch Size를 3으로 두면 detail에 대한 쿼리는 두 번만 실행된다IN 절에 최대 3개까지만 포함pring: jpa: properties: hibernate: default_batch_fetch_size: 10// Batch Fetch Size 적용 전 SIZE = 5 2025-03-17T16:53:55.480+09:00 INFO 4356 --- [ main] c.j.p.d.r.ProjectRepositoryTest : findAllByIsActive 테스트 시작 Hibernate: select p1_0.id,p1_0.created_date_time,p1_0.description,p1_0.end_month,p1_0.end_year,p1_0.is_active,p1_0.name,s1_0.project_id,s1_0.id,s1_0.created_date_time,s1_0.skill_id,s2_0.id,s2_0.created_date_time,s2_0.is_active,s2_0.name,s2_0.skill_type,s2_0.updated_date_time,s1_0.updated_date_time,p1_0.start_month,p1_0.start_year,p1_0.updated_date_time from project p1_0 left join project_skill s1_0 on p1_0.id=s1_0.project_id join skill s2_0 on s2_0.id=s1_0.skill_id where p1_0.is_active=? 2025-03-17T16:53:55.565+09:00 INFO 4356 --- [ main] c.j.p.d.r.ProjectRepositoryTest : projects.size: 5 Hibernate: select d1_0.project_id,d1_0.id,d1_0.content,d1_0.created_date_time,d1_0.is_active,d1_0.updated_date_time,d1_0.url from project_detail d1_0 where d1_0.project_id=? 2025-03-17T16:53:55.573+09:00 INFO 4356 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.details.size: 1 2025-03-17T16:53:55.574+09:00 INFO 4356 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.skills.size: 1 Hibernate: select d1_0.project_id,d1_0.id,d1_0.content,d1_0.created_date_time,d1_0.is_active,d1_0.updated_date_time,d1_0.url from project_detail d1_0 where d1_0.project_id=? 2025-03-17T16:53:55.577+09:00 INFO 4356 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.details.size: 2 2025-03-17T16:53:55.577+09:00 INFO 4356 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.skills.size: 2 Hibernate: select d1_0.project_id,d1_0.id,d1_0.content,d1_0.created_date_time,d1_0.is_active,d1_0.updated_date_time,d1_0.url from project_detail d1_0 where d1_0.project_id=? 2025-03-17T16:53:55.579+09:00 INFO 4356 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.details.size: 3 2025-03-17T16:53:55.582+09:00 INFO 4356 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.skills.size: 3 Hibernate: select d1_0.project_id,d1_0.id,d1_0.content,d1_0.created_date_time,d1_0.is_active,d1_0.updated_date_time,d1_0.url from project_detail d1_0 where d1_0.project_id=? 2025-03-17T16:53:55.588+09:00 INFO 4356 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.details.size: 4 2025-03-17T16:53:55.589+09:00 INFO 4356 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.skills.size: 4 Hibernate: select d1_0.project_id,d1_0.id,d1_0.content,d1_0.created_date_time,d1_0.is_active,d1_0.updated_date_time,d1_0.url from project_detail d1_0 where d1_0.project_id=? 2025-03-17T16:53:55.604+09:00 INFO 4356 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.details.size: 5 2025-03-17T16:53:55.606+09:00 INFO 4356 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.skills.size: 5 2025-03-17T16:53:55.607+09:00 INFO 4356 --- [ main] c.j.p.d.r.ProjectRepositoryTest : findAllByIsActive 테스트 종료// Batch Fetch Size 적용 후 SIZE = 5 2025-03-17T16:59:17.289+09:00 INFO 19160 --- [ main] c.j.p.d.r.ProjectRepositoryTest : findAllByIsActive 테스트 시작 Hibernate: select p1_0.id,p1_0.created_date_time,p1_0.description,p1_0.end_month,p1_0.end_year,p1_0.is_active,p1_0.name,s1_0.project_id,s1_0.id,s1_0.created_date_time,s1_0.skill_id,s2_0.id,s2_0.created_date_time,s2_0.is_active,s2_0.name,s2_0.skill_type,s2_0.updated_date_time,s1_0.updated_date_time,p1_0.start_month,p1_0.start_year,p1_0.updated_date_time from project p1_0 left join project_skill s1_0 on p1_0.id=s1_0.project_id join skill s2_0 on s2_0.id=s1_0.skill_id where p1_0.is_active=? 2025-03-17T16:59:17.369+09:00 INFO 19160 --- [ main] c.j.p.d.r.ProjectRepositoryTest : projects.size: 5 Hibernate: select d1_0.project_id,d1_0.id,d1_0.content,d1_0.created_date_time,d1_0.is_active,d1_0.updated_date_time,d1_0.url from project_detail d1_0 where d1_0.project_id in (?,?,?,?,?,?,?,?,?,?) 2025-03-17T16:59:17.385+09:00 INFO 19160 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.details.size: 1 2025-03-17T16:59:17.386+09:00 INFO 19160 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.skills.size: 1 2025-03-17T16:59:17.387+09:00 INFO 19160 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.details.size: 2 2025-03-17T16:59:17.387+09:00 INFO 19160 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.skills.size: 2 2025-03-17T16:59:17.387+09:00 INFO 19160 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.details.size: 3 2025-03-17T16:59:17.387+09:00 INFO 19160 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.skills.size: 3 2025-03-17T16:59:17.388+09:00 INFO 19160 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.details.size: 4 2025-03-17T16:59:17.388+09:00 INFO 19160 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.skills.size: 4 2025-03-17T16:59:17.390+09:00 INFO 19160 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.details.size: 5 2025-03-17T16:59:17.390+09:00 INFO 19160 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.skills.size: 5 2025-03-17T16:59:17.391+09:00 INFO 19160 --- [ main] c.j.p.d.r.ProjectRepositoryTest : findAllByIsActive 테스트 종료// Batch Fetch Size = 3, SIZE = 5 2025-03-17T17:09:35.183+09:00 INFO 944 --- [ main] c.j.p.d.r.ProjectRepositoryTest : findAllByIsActive 테스트 시작 Hibernate: select p1_0.id,p1_0.created_date_time,p1_0.description,p1_0.end_month,p1_0.end_year,p1_0.is_active,p1_0.name,s1_0.project_id,s1_0.id,s1_0.created_date_time,s1_0.skill_id,s2_0.id,s2_0.created_date_time,s2_0.is_active,s2_0.name,s2_0.skill_type,s2_0.updated_date_time,s1_0.updated_date_time,p1_0.start_month,p1_0.start_year,p1_0.updated_date_time from project p1_0 left join project_skill s1_0 on p1_0.id=s1_0.project_id join skill s2_0 on s2_0.id=s1_0.skill_id where p1_0.is_active=? 2025-03-17T17:09:35.245+09:00 INFO 944 --- [ main] c.j.p.d.r.ProjectRepositoryTest : projects.size: 5 Hibernate: select d1_0.project_id,d1_0.id,d1_0.content,d1_0.created_date_time,d1_0.is_active,d1_0.updated_date_time,d1_0.url from project_detail d1_0 where d1_0.project_id in (?,?,?) 2025-03-17T17:09:35.259+09:00 INFO 944 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.details.size: 1 2025-03-17T17:09:35.260+09:00 INFO 944 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.skills.size: 1 2025-03-17T17:09:35.261+09:00 INFO 944 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.details.size: 2 2025-03-17T17:09:35.261+09:00 INFO 944 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.skills.size: 2 2025-03-17T17:09:35.261+09:00 INFO 944 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.details.size: 3 2025-03-17T17:09:35.261+09:00 INFO 944 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.skills.size: 3 Hibernate: select d1_0.project_id,d1_0.id,d1_0.content,d1_0.created_date_time,d1_0.is_active,d1_0.updated_date_time,d1_0.url from project_detail d1_0 where d1_0.project_id in (?,?,?) 2025-03-17T17:09:35.264+09:00 INFO 944 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.details.size: 4 2025-03-17T17:09:35.265+09:00 INFO 944 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.skills.size: 4 2025-03-17T17:09:35.265+09:00 INFO 944 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.details.size: 5 2025-03-17T17:09:35.265+09:00 INFO 944 --- [ main] c.j.p.d.r.ProjectRepositoryTest : project.skills.size: 5 2025-03-17T17:09:35.266+09:00 INFO 944 --- [ main] c.j.p.d.r.ProjectRepositoryTest : findAllByIsActive 테스트 종료아쉬운 점금요일부터 이번 주 강의를 듣게 됐는데 생각보다 오래 걸려서 섹션 4까지 강의를 다 못 들은 게 아쉽다 오래 걸린 이유늦게 듣기 시작강의를 들으면서 부족한 내용 구글링 및 보완Trouble Shooting 내용 정리 다음 주는 진도가 뒤처지지 않도록 매일 들어야겠다보완할 점아직은 Kotlin으로 Project를 하는 것이 낯설고 어렵게 느껴진다Kotlin 문법에 얼른 익숙해지도록 공부해야겠다강의 매일 듣기Kotlin 문법 공부Kotlin Spring Project 찾아보기혼자 테스트 코드 작성해보기Trouble Shooting/h2-console 접속 오류findById 최적화 궁금증 해결회고테스트 코드를 작성하는 건 역시 어려운 것 같다Java Spring Project를 했을 때도 테스트 코드를 작성하는 것은 어려웠다일단 이번 주에 작성했던 Repository 테스트 코드를 다시 살펴보고, 테스트 코드를 잘 작성하려면 어떻게 공부를 해야 하는지 질문을 남겨봐야겠다이번 주는 유독 어려웠던 주차였지만, 동시에 좀 더 성장할 수 있었던 주차였다스터디 수료 후에는 테스트 코드와 친해져 있었으면 좋겠다 🎯 미션 2테이블 설계하기ERD, 표 등 테이블 설계한 내용을 readme 파일에 작성커밋 메시지 : [미션2] 테이블 설계하기미션2 제출 스레드에 깃허브 커밋 링크를 공유문제학사 관리 서비스를 주제로 선택했을 때, 학생이 여러 과목을 수강 신청할 수 있는 기능만 CRUD로 구성해 가볍게 미니 프로젝트를 진행하려고 했다하지만 수강 신청 기능만 봤을 때 U 부분을 어떻게 해야 할지 많이 고민 됐다C : 수강 신청R : 수강 신청 목록 조회D : 수강 신청 취소결국, 처음 의도와 달리 기능이 확장되면서 테이블도 증가하게 되었다그리고 설계를 하면서 각 테이블 컬럼도 증가하게 되었다2T → 4T해결 도전해결은 아니지만, 설계한 대로 진행해 보려고 한다Kotlin은 처음이지만, Spring Project는 처음이 아니기 때문에 Kotlin을 사용해 학사 관리 서비스의 기능 구현을 마무리하는 것을 이번 미니 프로젝트의 목표로 삼았다회고처음 주제를 선택했을 때, 기능 구현에 대해 단순하게 생각했다그 결과, 테이블을 설계할 때 처음 의도와는 다른 구조가 만들어졌다주제에 따른 기능을 충분히 검토하고, 기한 내에 구현할 수 있는 기능인지 고민했어야 했는데, 한편으로는 자만했던 부분도 있었다Kotlin 문법만 금방 익히면, Spring 프로젝트는 처음이 아니었기 때문에 추후 기능이 확장되더라도 문제없을 거라고 안심했던 것 같다 🎯 미션 3REST API 설계하기API를 설계한 내용을 readme 파일에 작성한 뒤 커밋커밋 메시지 : [미션3] REST API 설계하기미션3 제출 스레드에 깃허브 커밋 링크를 공유문제세부 기능이 정리가 안돼 설계할 때 수정을 자주 했다해결권한별로 기능을 분리하니 이전보다 명확해져 세부 기능을 정리하기가 쉬웠다덕분에 URL을 설정하는 것도 훨씬 수월해졌다공통 기능학생 기능prefix: /students교수 기능prefix: /professors관리자 기능prefix: /admins 회고보통 도메인별로 기능을 정리해 설계하는 것 같고, 나도 항상 그렇게 해왔다하지만 이번 서비스에서는 각 권한에 따른 역할이 뚜렷하기 때문에 권한에 따른 URL 경로를 설정하는 방식으로 하였고, 권한별로 분리하니 각 기능이 직관적으로 보였다 서비스가 커지면 권한에 따라 기능이 세분화되어 URL 경로가 복잡해질 수 있고, 오히려 마이너스가 될 수 있다고 생각했다주요 기능이 소수이되 규모가 작은 미니 프로젝트인 경우에만 개인적으로 사용해야겠다
백엔드
2025. 03. 09.
0
인프런 워밍업 클럽 3기 BE 스터디 1주차
💻 강의입문자를 위한 Spring Boot with Kotlin - 나만의 포트폴리오 사이트 만들기 📚 학습프로젝트 구조├─kotlin │ └─com │ └─jiyeon │ └─portfolio │ └─domain │ ├─constant │ ├─entity │ └─repository └─resources ├─static └─templates BaseEntityBaseEntity를 추상 클래스로 만들고, 상속 받아서 사용한다@MappedSuperclass abstract class BaseEntity { @CreatedDate @Column(nullable = false, updatable = false) var createdDateTime: LocalDateTime = LocalDateTime.now() @LastModifiedDate @Column(nullable = false) var updatedDateTime: LocalDateTime = LocalDateTime.now() }@Entity class Achievement( title: String, description: String, achievedDate: LocalDate?, host: String, isActive: Boolean) : BaseEntity() { ... }Experience와 ExperienceDetail 연관 관계Experience와 ExperienceDetail은 1 : N 관계를 가진다Experience에서는 MutableList 타입의 details 필드로 ExperienceDetail을 가져올 수 있지만, ExperienceDetail에서는 매핑된 Experience를 가져올 수 없다 (단방향)즉, Experience와 ExperienceDetail은 1 : N 단방향 관계를 가진다@Entity class Experience( title: String, description: String, startYear: Int, startMonth: Int, endYear: Int?, endMonth: Int?, isActive: Boolean) : BaseEntity() { ... @OneToMany( targetEntity = ExperienceDetail::class, fetch = FetchType.LAZY, cascade = [CascadeType.ALL]) @JoinColumn(name = "experience_id") var details: MutableList = mutableListOf() ... }@Entity class ExperienceDetail( content: String, isActive: Boolean) : BaseEntity() { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null var content: String = content var isActive: Boolean = isActive }mappedBy양방향 연관 관계에서 주인을 정할 때 mappedBy를 사용해 지정한다@OneToMany(mappedBy = "project", fetch = FetchType.LAZY, cascade = [CascadeType.PERSIST]) var skills: MutableList = mutableListOf()@Entity class ProjectSkill(project: Project, skill: Skill) : BaseEntity() { ... @ManyToOne(targetEntity = Project::class, fetch = FetchType.LAZY) @JoinColumn(name = "project_id", nullable = false) var project: Project = project ... } 아쉬운 점다른 Project를 같이 하다 보니 늦게 강의를 듣게 됐고, 다급하게 과제를 시작했다보완할 점Kotlin 문법이 아직 낯설어서 학습 속도가 느린 것 같다Project에서 사용하는 Kotlin 문법을 같이 공부해 학습 속도를 개선하고, 일정에 차질이 없도록 해야겠다궁금한 점1주차 강의를 들으면서 궁금했던 점은 Entity 필드에 private 접근 제한자를 사용하지 않았던 부분이다Java를 사용해 Project를 했을 때는 외부에서 접근하지 못하도록 Entity 필드에 private 접근 제한자를 사용했는데 강의에서는 Entity에 접근 제한자를 사용하지 않았다이 부분에서 Java와 Kotlin으로 Entity를 개발하는 것이 다르다고 느껴졌다// Java @Entity @Table(name = "user") @Getter @NoArgsConstructor @SQLDelete(sql = "UPDATE user SET is_deleted = true WHERE id = ?") public class User extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(length = 50, nullable = false, unique = true) private String email; @Column(length = 100, nullable = false) private String password; @Column(length = 20, nullable = false) private String nickname; @Column(nullable = false) @Enumerated(EnumType.STRING) private UserRole role; private boolean isDeleted = false; public User(String email, String password, String nickname, String role) { this.email = email; this.password = password; this.nickname = nickname; this.role = UserRole.of(role); } }// Kotlin @Entity class ExperienceDetail( content: String, isActive: Boolean) : BaseEntity() { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null var content: String = content var isActive: Boolean = isActive }회고Java를 사용해 Spring Project는 해봤지만, Kotlin을 사용해 Project를 하는 것은 이번이 처음이어서 익숙한 듯 새롭고, 재미있지만 조금 어렵다어렵게 느껴진 이유는 Kotlin 문법에 익숙해지지 않아서 그런 것 같다Kotlin을 사용한 여러 Spring Project도 많이 찾아보면서 참고해야겠다 🎯 미션 1코틀린/스프링 부트 프로젝트를 생성깃허브 리포지토리에 프로젝트 올리기커밋 메시지 : [미션1] 깃허브 리포지토리에 프로젝트 올리기미션1 제출 스레드에 깃허브 리포지토리 링크를 공유문제미니 프로젝트 주제에 대해 많이 고민 됐고, 일정 지연으로 인해 미션1 제출을 아직 못한 상태이다해본 적 없는 주제이면서 Kotlin으로 처음 하는 것이기 때문에 어렵지 않은 주제로 선정하려고 한다해결고민한 끝에 노션에 있는 주제 중 하나인 학사 관리 서비스 주제를 선택했다DB 정규화 관련해 찾아볼 때, 학생 - 수강 과목 관계로 예시를 든 래퍼런스가 많아서 학사 관리 서비스 주제가 무난하다고 생각했다회고인프런 워밍업 클럽 BE 스터디 이외에도 개인적으로 하려는 Project가 있기 때문에 놓치는 일정이 생길 때도 있다일정을 놓치지 않도록 GitHub Project를 이용하고, Kotlin Spring Project와 Java Spring Project 둘 다 잘 마무리 하려고 한다
백엔드