게시글
블로그
전체 52025. 03. 16.
0
[인프런 워밍업 클럽 3기] 백엔드 발자국 2주차
📒 2주차에 배운 내용 📌 Controller, Service , Repository , DTO 개발과 테스팅, 그리고 Tymeleaf fragments 에 대해 학습하는 시간이였다.✏ 강의 중에 다뤘던 PresentationService 테스트 @DisplayName("활성화된 Introduction 만 조회하는데 성공한다. ") @Test fun testGetIntroductions() {/* given */ val introductions = mutableListOf() (1..DATA_SIZE).forEach { introductions.add(Introduction(content = "$it", isActive = it % 2 == 0)) } val activeIntroductions = introductions.filter { it.isActive } Mockito.`when`(presentationRepository.getActiveIntroductions()).thenReturn(activeIntroductions) /* when */ val introductionDTOs = presentationService.getIntroductions() /* then */ assertThat(introductionDTOs).hasSize(DATA_SIZE / 2) introductionDTOs.forEach { assertThat(it.content.toInt()).isEven() } } @DisplayName("활성화된 Link 만 조회하는데 성공한다. ") @Test fun testGetLinks() {/* given */ val links = mutableListOf() (1..DATA_SIZE).forEach { links.add(Link(name = "$it", content = "$it", isActive = it % 2 != 0)) } val activeLinks = links.filter { it.isActive } Mockito.`when`(presentationRepository.getActiveLinks()).thenReturn(activeLinks) /* when */ val linkDTOs = presentationService.getLinks() /* then */ var expectedSize = DATA_SIZE / 2 if (DATA_SIZE % 2 != 0) expectedSize += 1 assertThat(linkDTOs).hasSize(expectedSize) linkDTOs.forEach { assertThat(it.content.toInt()).isOdd() } } ✏ Repository 성능 개선 -> N + 1 해결하기 (fetch join)@Repository interface ProjectRepository: JpaRepository { @Query("select distinct p from Project p left join fetch p.details where p.id = :id") override fun findById(id: Long): Optional }📌 JPQL fetch Joinspring: application: name: kotlin-portfolio jpa: database: h2 open-in-view: false show-sql: true hibernate: ddl-auto: create properties: hibernate: format_sql: true default_batch_fetch_size: 10 # batch size 조절 📌`application.yml` 에서 batch size 설정 ✏ Mocking 을 활용한 Controller 테스트val logger = KotlinLogging.logger {} @AutoConfigureMockMvc @SpringBootTest @DisplayName("[API 컨트롤러 테스트]") class PresentationApiControllerTest ( @Autowired private val mockMvc: MockMvc, ){ @DisplayName("Introductions 조회") @Test fun testGetIntroductions() { /* given */ val uri = "/api/v1/introductions" /* when */ val mvcResult = performGet(uri) val contentAsString = mvcResult.response.contentAsString val jsonArray = JSONArray(contentAsString) /* then */ assertThat(jsonArray.length()).isPositive() } @DisplayName("Links 조회") @Test fun testGetLinks() { /* given */ val uri = "/api/v1/links" /* when */ val mvcResult = performGet(uri) val contentAsString = mvcResult.response.contentAsString val jsonArray = JSONArray(contentAsString) /* then */ assertThat(jsonArray.length()).isPositive() } @DisplayName("Resume 조회") @Test fun testGetResume() { /* given */ val uri = "/api/v1/resume" /* when */ val mvcResult = performGet(uri) val contentAsString = mvcResult.response.contentAsString val jsonObject = JSONObject(contentAsString) /* then */ logger.info{jsonObject.optJSONObject("experiences")} assertThat(jsonObject.optJSONArray("experiences").length()).isPositive() assertThat(jsonObject.optJSONArray("achievements").length()).isPositive() assertThat(jsonObject.optJSONArray("skills").length()).isPositive() } private fun performGet(uri: String): MvcResult { return mockMvc .perform (MockMvcRequestBuilders.get(uri)) .andDo(MockMvcResultHandlers.print()) .andReturn() } }📌 2주차 미션 1 대 다 관계를 갖는 테이블 설계하기 ✔Rest API 를 설계하기✔
2025. 03. 10.
0
[인프런 워밍업 클럽 3기] 백엔드 발자국 1주차
스프링 의존성 주입### 1. 생성자```kotlin@ServiceClass PresentaionService(private val presentaionRepository: PresentaionRepository){// 생략}```> 생성자 주입 방식을 권장하는 3가지 이유> 1. 런타임에 수정자를 호출해서 의존성이 바뀌는 것을 방지할 수 있다.> 2. 순환참조 시 컴파일 오류가 발생해서 런타임 단계에서 메소드가 서로 호출하는 스택오버플로우 에러를 방지할 수 있다.> 3. 의존하는 Bean 이 누락되면 컴파일 오류가 발생하기 때문에 런타임에서 NullPointerException 을 방지할 수 있다.### 2. 수정자```kotlin@ServiceClass PresentaionService {private lateinit var presentaionRepository: PresentaionRepository@Autowiredfun setPresentationRepository(presentaionRepository: PresentaionRepository){this.presentaionRepository = presentaionRepository}}```### 필드주입```kotlin@ServiceClass PresentaionService {@Autowiredprivate lateinit val presentaionRepository: PresentaionRepository}```HTTP와 REST API### HTTP 정의> [!tip] HTTP 정의> * Hyper Text Transfer Protocol> * 네트워크로 통신하는 두 컴포넌트 간의 통신 규약### HTTP 요청/응답> Request> * Start Line: HTTP 메서드, URL, HTTP 버전을 표시한다.> * Header: 컨텐츠의 길이나 유형, 클라이언트 (브라우저) 의 정보 등을 표현한다.> * Body: 서버에서 작업을 처리하기 위해 필요한 실질적인 데이터를 담는다. 최근에는 JSON 포맷을 주로 사용한다.> Resposne> * Start Line : HTTP 버전, 상태 코드, 상태 메세지를 표현한다.> * Header: 컨텐츠의 길이나 유형, 서버의 정보 등을 표현한다.> * Body : 응답의 결과 데이터를 담는다. 서버 사이드 렌더링 방식일 경우 HTML 을 사용한다.> 클라이언트 사이드 렌더링 방식이거나 서버 간의 통신일 경우 주로 JSON 을 사용한다.### HTTP 요청 메서드> GET> * READ 작업을 요청할 때 사용한다.> * 브라우저의 주소창은 항상 GET 메서드로 요청한다.> * GET 요청을 할 경우 일부 HTTP 라이브러리에서는 Body 에 데이터를 담을 수 없다.> * 따라서 데이터를 보내야 할 경우 쿼리 파라미터를 주로 사용한다.> POST> * Creat 작업을 요청할 때 사용> * 브라우저에서는 사용할 수 없고, Postman 과 같은 별도의 툴을 사용해야 한다.> PUT> * Update 작업을 요청할 때 사용> PATCH> * Update 작업을 요청할 때 사용> DELETE> * Delete 작업을 요청할 때 사용## 데이터베이스### 데이터베이스 정의> 📌 여러 사람이 공유하여 사용할 목적으로 체계화해 통합, 관리하는 데이터의 집합### DBMS (DataBase Management System)> 📌 데이터의 집합(데이터베이스)을 저장하고 관리할 수 있는 응용 프로그램.### 관계형 데이터베이스> 📌 가장 널리 쓰이는 데이터베이스. 데이터를 행과 열로 이루어진 표의 형태로 저장함.> 각 테이블 들은 관계를 가지기 때문에 관계형 데이터베이스라고 함.#### 대표적인 관계형 데이터베이스> 📒 오라클 ,`Mysql`, PostgreSQl### 비관계형 Database> 📒 mongoDB, ... Redis### JPA> 📌 Java Persistance API 약어, 자바 ORM 기술의 표준 인터페이스### ORM> 📌 Objecte Relational Mapping 약어 , 객체 관계 매핑 -> 객체 지향 프로그래밍의 인스턴스와 관계형 데이터베이스를 매핑해주는 기술을 의미.### 트랜잭션> 📌 여러개의 데이터베이스 작업을 하나로 묶어주는 논리적인 단위.궁금했던 점엔티티 생성 시, 생성자 선언방식을 사용해도 되는가?> 예시 -> @Entity class Achievement( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "achievement_id") val id: Long? = null, var title: String, var description: String, var achievedDate: LocalDate? = null, var host: String, var isActive: Boolean, ): BaseEntity()위와 같이 엔티티를 생성방식은 중복되는 코드를 줄일 수 있어서 개인적으로 선호하는 방식인데, 이런 방식을 사용해도 되는지 궁금증이 생겼다. [입문자를 위한 Spring Boot with Kotlin](https://www.inflearn.com/course/%EC%9E%85%EB%AC%B8%EC%9E%90-spring-boot-kotlin-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4/dashboard)
워밍업클럽3기
2024. 03. 08.
0
[인프런 워밍업 클럽 0기 BE] - 세 번째 발걸음
미니 프로젝트 Step 02구현 내용* ①출근 기능* 등록된 직원은 출근을 할 수 있어야 한다. 출근의 경우 이름은 동명이인이 있을 수 있으므로, DB에 등록된 ID를 기준으로 처리된다.* ②퇴근 기능* 출근한 직원은 퇴근을 할 수 있어야 한다. 퇴근 역시 DB에 등록된 ID르ㅜㄹ 기준으로 처리된다.* ③특정 직원의 날짜별 근무시간을 조회하는 기능* 특정 직원 id와 2024-01과 같이 연/월을 받으면, 날짜별 근무 시간과 총 합을 반환해야 한다. 이때 근무 시간은 분단위로 계산된다.* 예를 들어, 1번 id를 갖는 직원에 대해 2024-01을 기준으로 조회하면, 다음과 같은 응답이 반환되어야 한다.{ "detail": [ { "date": "2024-01-01", "workingMinutes": 480 }, { "date": "2024-01-02", "workingMinutes": 490 }, ... // 2024년 1월 31일까지 존재할 수 있다. ] "sum": 10560 } 📌 edge-case> - 등록되지 않은 직원이 출근 하려는 경우> - 출근한 직원이 또 다시 출근하려는 경우> - 퇴근하려는 직원이 출근하지 않았던 경우> - 그 날, 출근했다 퇴근한 직원이 다시 출근하려는 경우 --- 과정* Table> 💡 고민 1.테이블을 어떻게 짤까?CREATE TABLE commute ( id bigint auto_increment, start_of_work datetime, end_of_work datetime, attendance tinyInt, member_id bigint, primary key (id) ); > 📌 JPA Auditing저번 피드백에서 @EntityListeners(AuditingEntityListener.class) 어노테이션을 사용하는BaseEntity 추상 클래스를 만들어 봤는데, BaseEntity의 CreatedAt, UpdatedAt 필드를 상속받고 출/퇴근시간을 자동으로 기록하는게 개인적인 목표입니다!*Controller @RestController @RequiredArgsConstructor public class CommuteController { private final CommuteService commuteService; @PostMapping("/start-of-work") public void startOfWork(@Valid @RequestBody startOfWorkRequest request) { commuteService.startOfWork(request); }@PostMapping("/end-of-work") public void endOfWork(@Valid @RequestBody endOfWorkRequest request) { commuteService.endOfWork(request); }@GetMapping("/commute") public ResponseEntity GetCommuteRecord(@Valid GetCommuteRecordRequest request){ GetCommuteRecordResponse getCommuteRecordResponse = commuteService.GetCommuteRecord(request); return ResponseEntity.ok().body(getCommuteRecordResponse); } } > 📌 클래스를 매개변수로 사용하는 경우클래스 형태의 객체를 매개변수로 받는 컨트롤러 메소드에서 별도의 어노테이션을 사용하지 않는 경우,스프링은 기본적으로 쿼리 파라미터를 클래스의 프로퍼티와 매핑한다.@RequestParam 어노테이션을 사용하면 매개변수가 쿼리 파라미터로 넘어오는 것이 아니라, 매개변수 자체가 요청의 특정 파라미터와 매핑되도록 기대한다.따라서 클래스 타입의 객체를 @RequestParam으로 직접 받는다면 쿼리 파라미터 매핑이 자동으로 이뤄지지 않는다. DTO(Request)public record endOfWorkRequest(@NotNull long id) {public record GetCommuteRecordRequest(@NotNull long id, @DateTimeFormat(pattern = "yyyy-MM") YearMonth yearMonth) { }public record startOfWorkRequest(@NotNull long id) { public Commute toEntity(Member member){ return Commute.builder() .member(member) .build(); }💡 저번 피드백에서 record 사용법을 배워서 record로 생성하였습니다.* Domain@Getter @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @AttributeOverrides({ @AttributeOverride(name= "createdAt", column = @Column(name= "start_of_work")), @AttributeOverride(name= "updatedAt", column = @Column(name= "end_of_work")) }) public class Commute extends BaseEntity { //BaseEntity를 상속받음 @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; private boolean attendance = true; // 출근 상태 @ManyToOne(fetch= FetchType.LAZY) private Member member; public void endOfWork(){ this.attendance = false; } @Builder public Commute(Member member){ this.member = member; } } 💡 저번 피드백에서 @EntityListeners(AuditingEntityListener.class) 어노테이션을 사용하는추상 클래스 생성을 권유 받았는데, 만들고 나니 써먹고 싶어져서 추상 클래스를 상속받고, attendance값의 변경을 통해 출/퇴근을 구현해보려고 합니다. 💡 처음에는 Member 도메인과 1 : N 양방향 연관 관계로 설계를 해뒀었는데,블로그를 작성하면서 확인해보니, Member에 맺어둔 @OneToMany를 전혀 사용을 안했다는걸 깨닫고, Commute의 @ManyToOne 단방향 연관관계만 살려두었습니다.* Service@Service @Slf4j @RequiredArgsConstructor public class CommuteService { private final CommuteRepository commuteRepository; private final MemberRepository memberRepository;@Transactional public void startOfWork(startOfWorkRequest request) { Member member = findMemberById(request.id()); Commute latestCommute = findLatestCommuteByMember(member); if (latestCommute.isAttendance()) throw new AbsentCheckOutException(); //이전 기록 퇴근확인 boolean isAlreadyAttendance = LocalDate.now().equals(LocalDate.from(latestCommute.getCreatedAt())); if (isAlreadyAttendance) throw new AlreadyAttendanceException(); //당일 출근기록 확인 commuteRepository.save(request.toEntity(member)); }@Transactional public void endOfWork(@RequestBody endOfWorkRequest request) { Member member = findMemberById(request.id()); Commute latestCommute = findLatestCommuteByMember(member); if (!latestCommute.isAttendance()) throw new AlreadyDepartureException(); latestCommute.endOfWork(); //변경감지 자동저장 }@Transactional public GetCommuteRecordResponse GetCommuteRecord(GetCommuteRecordRequest request) { findMemberById(request.id()); List commuteDetailList = findCommuteListByMemberIdAndStartOfWork(request); Long sum = commuteDetailList.stream() .map(GetCommuteDetail::workingMinutes) .reduce(0L, Long::sum); //commuteDetailList에서 workingMinutes를 조회, reduce로 합을 반환 return new GetCommuteRecordResponse(commuteDetailList, sum); } private Member findMemberById(long id) { return memberRepository.findById(id).orElseThrow(MemberNotFoundException::new); } private Commute findLatestCommuteByMember(Member member) { return commuteRepository.findLatestCommuteByMemberId(member.getId()) .orElseThrow(CommuteNotFoundException::new); } private List findCommuteListByMemberIdAndStartOfWork(GetCommuteRecordRequest request) { List commuteList = commuteRepository.findCommuteListByMemberIdAndStartOfWork(request.id(), request.yearMonth().getYear(), request.yearMonth().getMonth().getValue()); if (commuteList.isEmpty()) throw new CommuteNotFoundException(); //해당범위에 통근기록 존재 X return commuteList.stream().map(GetCommuteDetail::from).toList(); //CommuteDetail으로 변환 } } 💡**고민 2.**현재 startOfWork의 isAlreadyAttendance(전 기록 퇴근처리 확인) 기능이 과연 필요한가 고민중입니다.야근 후, 12시가 넘어서 퇴근을 찍지않고 출근을 찍을 경우를 대비해서 넣어뒀는데,퇴근을 찍지않으면 그 날의 CreatedAt(출근시간)과 UpdatedAt(퇴근시간) 이 동일하여근무시간이 0으로 찍히기 떄문에 본인이 알아서 인사팀에 찾아가지 않을까요? 🤔* DTO(Reponse)@Builder public record GetCommuteDetail(LocalDate date, long workingMinutes) { public static GetCommuteDetail from(Commute commute){ Duration duration = Duration.between(commute.getCreatedAt(), commute.getUpdatedAt()); return GetCommuteDetail.builder() .date(commute.getCreatedAt().toLocalDate()) .workingMinutes(duration.toMinutes()) .build(); } }public record GetCommuteRecordResponse(List detail, long sum) { }💡 DTO 반환 과정에서 Duration을 활용해 생성시간과 수정시간의 차이를 분으로 바꿔줬습니다.* Repositorypublic interface CommuteRepository extends JpaRepository { @Query("SELECT latestcommute FROM Commute latestcommute WHERE latestcommute.member.id = :memberId AND latestcommute.createdAt = (SELECT MAX(commute.createdAt) FROM Commute commute WHERE commute.member.id = :memberId)") Optional findLatestCommuteByMemberId(Long memberId); @Query("SELECT commute FROM Commute commute WHERE commute.member.id= :memberId AND FUNCTION('YEAR', commute.createdAt)= :year AND FUNCTION('MONTH', commute.createdAt)= :month") List findCommuteListByMemberIdAndStartOfWork(Long memberId, int year, int month); } 💡 다른분들께 배운 JPQL 활용해보기* findLatestCommuteByMemberId = SELECT MAX를 통해 가장 최근의 통근 기록을 조회한다.* findCommuteListByMemberIdAndStartOfWork = GetCommuteRecordRequest 의①`MemberId`, ②`year(request.yearMonth().getYear())`, ③`month(request.getMonth().getValue())`값을 만족하는 모든 Commute를 조회한다. # 구현 결과 ①출근 기능 등록된 직원은 출근을 할 수 있어야 한다. 출근의 경우 이름은 동명이인이 있을 수 있으므로, DB에 등록된 ID를 기준으로 처리된다. ②퇴근 기능 출근한 직원은 퇴근을 할 수 있어야 한다. 퇴근 역시 DB에 등록된 ID를 기준으로 처리된다. ③특정 직원의 날짜별 근무시간을 조회하는 기능 특정 직원 id와 2024-01과 같이 연/월을 받으면, 날짜별 근무 시간과 총 합을 반환해야 한다. 이때 근무 시간은 분단위로 계산된다. 예를 들어, 1번 id를 갖는 직원에 대해 2024-01을 기준으로 조회하면, 다음과 같은 응답이 반환되어야 한다. ④edge-case 등록되지 않은 직원이 출근 하려는 경우 출근한 직원이 또 다시 출근하려는 경우 퇴근하려는 직원이 출근하지 않았던 경우 그 날, 출근했다 퇴근한 직원이 다시 출근하려는 경우 코드 리뷰 Step 02피드백 1.@Query("SELECT latestcommute FROM Commute latestcommute WHERE latestcommute.member.id = :memberId AND latestcommute.createdAt = (SELECT MAX(commute.createdAt) FROM Commute commute WHERE commute.member.id = :memberId)")  📌 서브 쿼리를 이용해 가장 최근 기록을 가져오셨군요! 생각하지 못한 방법 또 하나 배우고 갑니다! 😊추가로 질문을 드리자면 저 같은 경우는 서브쿼리는 성능이 걱정되어 불가피한 상황이 아닌 경우에는 지양하는 편인데 영훈님은 어떻게 생각하시는지 궁금합니다...! 해결 과정 1. 1. JPQL을 활용해 Join ORDER BY LIMIT 으로 변경 우선 쿼리문을 수정했습니다.@Query("SELECT latestcommute FROM Commute latestcommute JOIN latestcommute.member member WHERE member.id = :memberId ORDER BY latestcommute.createdAt DESC") 하지만 LIMIT를 활용하려 하니, JPQL 자체적으론 LIMIT를 지원하지 않는다는 사실을 알게 되었습니다.@Query("SELECT latestcommute FROM Commute latestcommute JOIN latestcommute.member member WHERE member.id = :memberId ORDER BY latestcommute.createdAt DESC") List findFirstByMemberId(long MemberId);# 실제 쿼리 조회 Hibernate: select c1_0.id, c1_0.attendance, c1_0.start_of_work, c1_0.member_id, c1_0.end_of_work from commute c1_0 join member m1_0 on m1_0.id=c1_0.member_id where m1_0.id=? order by c1_0.start_of_work desc 받아온 List를 Service 단에서 처리하려다, 모든 List를 받아오는것이 마음에 들지 않아서 JPA에 대해 조금 더 찾아보았습니다.2. JPA 쿼리 메서드 활용 JPQL에서 LIMIT을 지원하지 않는데, 굳이 JPQL을 사용할 이유가 없었습니다.Optional findFirstByMemberIdOrderByCreatedAtDesc(Long memberId); 해당 쿼리 메서드 조회시 실제 전송되는 쿼리문Hibernate: select c1_0.id, c1_0.attendance, c1_0.start_of_work, c1_0.member_id, c1_0.end_of_work from commute c1_0 left join member m1_0 on m1_0.id=c1_0.member_id where m1_0.id=? order by c1_0.start_of_work desc limit ?💡 서브쿼리를 사용하지 않고, Left Join을 통해 최근순으로 정렬하고, LIMIT을 통해 제일 처음(가장 최근)`Commute`을 반환합니다.피드백 2.public record startOfWorkRequest(@NotNull long id) { public Commute toEntity(Member member){ return Commute.builder() .member(member) .build(); } }📌 요청값 검증 처리를 위해 @NotNull 등 어노테이션을 사용하고 계신데,요청값에 대해 예외가 발생하면 어떤식으로 응답이 나가는지 알고 계신가요?몇몇 커스텀 예외에 대해 핸들링 해서 일관된 응답 형식으로 응답이 나가고 있는데,요청값 검증 예외는 다른 형식으로 응답이 나갈것 같습니다.해결 과정 2.Edge-Case의 Exception을 우선적으로 설정하느라 @Valid 로 값 검증을 하고 있으면서도ExceptionHandler 에서 정작 ValidException 처리하는 메서드를 만들어 놓지 않았다는걸 깨달았습니다.따로 ValidException 처리를 하지 않았기때문에, 요청값 검증 예외가 발생한다면,전부 500 Internal-server-error로 처리 되었을 것입니다. @ExceptionHandler(MethodArgumentNotValidException.class) protected ResponseEntity handle(MethodArgumentNotValidException e){ e.getStackTrace(); log.error("MethodArgumentNotValidException", e); return createErrorResponse(ErrorCode.INVALID_INPUT_VALUE); }💡 MethodArgumentNotValidException 예외가 발생한다면, 400 Bad Request와 올바르지 않은 입력값이라는 메세지가 출력되도록 했습니다.미니 프로젝트 Step 03구현 내용연차 신청 이제부터 직원은 연차를 신청할 수 있습니다.연차는 무조건 하루 단위로만 사용이 가능합니다.올해 입사한 직원은 11개의 연차를, 그 외 직원은 15개의 연차를 사용할 수 있습니다.연차를 사용하기 위해서는 연차 사용일을 기준으로 며칠전 연차 등록을 해야 합니다.연차를 등록하기만 하면, 매니저의 허가 없이 연차가 바로 적용됩니다.단, 며칠 전에 연차를 등록해야 하는지는 팀 마다 다르게 적용됩니다.예를 들어 A팀은 하루 전에만 등록하면 연차를 사용할 수 있지만, B팀은 7일 전에 등록해야 연차를 사용할 수 있습니다.연차 조회 각 직원은 id를 이용해 올해 사용하지 않고 남은 연차를 확인할 수 있습니다.특정 직원의 날짜별 근무시간을 조회하는 기능 Version02연차를 신청할 수 있게되며, project_Step02 에서 개발했던 기능도 조금 변경되어야 합니다.만약 연차를 사용했다면, UsingDayOff : true가 반환되어야 합니다. { "detail": [ { "date": "2024-01-01", "workingMinutes": 480, "usingDayOff": false // 연차를 사용하지 않았으니, false가 반환 }, { "date": "2024-01-02", "workingMinutes": 0, "usingDayOff": true // 연차를 사용한 날은 true가 반환 }, ... // 2024년 1월 31일까지 존재할 수 있다. ] "sum": 10560 } > 📌 edge-case연차를 사용한 직원이 출근하려는 경우 각 팀별 설정 연차 등록일 이전에 연차를 사용하려하는 경우해당일에 이미 연차를 등록한 경우 과거로 연차를 사용하려 하는 경우올해의 연차를 모두 사용한 경우과정💡 고민 1.연차 신청과 연차 조회는 쉽게 만들 수 있을거 같은데..특정 직원의 날짜별 근무시간을 조회하는 기능은 어떻게 처리할지, 만약 그 방법으로 처리한다면필요한 Column은 무엇인지, 비즈니스 로직은 어디서 어떻게 처리할지가 고민이었습니다.TableCREATE TABLE annual ( id bigint auto_increment, annual_date_leave datetime, member_id bigint, primary key (id) ); 📌 어떻게 구현할지 생각해보기 Step02에서 만들었던 특정 직원의 날짜별 근무시간을 조회하는 기능을 처리할 때,해당 연월에 연차를 사용했는지 체크하고, 연차 사용기록이 존재한다면 연차를 사용한 요일 : {date}, 일한 시간 : 0, usingDayOff : true 로 반환해주려 합니다. 올해 사용하지 않고 남은 연차 조회 기능은 간단합니다. LocalDate.now() 로 구한 현재 년도와 MemberId 로 사용한 연차의 갯수를 구하고, ChronoUnit.Years.between을 활용해 입사년도와 LocalDate.now() 의 차이가 1보다 크거나 같다면 15, 그렇지 않다면 11 에서 위에서 구한 사용한 연차의 갯수를 빼주면 됩니다.만약 연차의 개수가 0보다 작거나 같다면, CustomException으로 예외를 던져주겠습니다.CREATE TABLE annual(id bigint auto_increment,annual_date_leave datetime,member_id bigint,primary key (id)); 📌 기존 team 테이블 수정팀별 연차 등록일을 설정해주기 위해 team 테이블을 수정하였습니다Controller@RestController @RequiredArgsConstructor public class AnnualLeaveController { private final AnnualLeaveService annualLeaveService; @PostMapping("/annual") public ResponseEntity registerAnnualLeave(@RequestBody @Valid RegisterAnnualLeaveRequest request) { annualLeaveService.registerAnnualLeave(request); return ResponseEntity.status(HttpStatus.CREATED).build(); } @GetMapping("/annual") public ResponseEntity getRemainAnnualLeaves(@Valid GetRemainAnnualLeavesRequest request) { long remainAnnualLeaves = annualLeaveService.getRemainAnnualLeaves(request); GetRemainAnnualLeavesResponse response = new GetRemainAnnualLeavesResponse(remainAnnualLeaves); return ResponseEntity.ok(response); } }@RestController @RequiredArgsConstructor public class TeamController { private final TeamService teamService; @PostMapping("/team") public ResponseEntity createTeam(@RequestBody CreateTeamRequest request) { teamService.createTeam(request); return ResponseEntity.status(HttpStatus.CREATED).build(); } @GetMapping("/team") public ResponseEntity> getAllTeams() { List allTeamsList = teamService.getAllTeams(); return ResponseEntity.ok().body(allTeamsList); } @PutMapping("/team/day-before-annual") public void updateDayBeforeAnnual(@RequestBody @Valid UpdateDayBeforeAnnualRequest request){ teamService.updateDayBeforeAnnual(request); } } 💡 각 팀별 연차 등록일 설정을 위해 TeamController 에 updateDayBeforeAnnual 메서드를추가하였습니다.DTOAnnualLeavepublic record GetRemainAnnualLeavesRequest(@NotNull long id) { }public record RegisterAnnualLeaveRequest(@NotNull long id, @Future LocalDate date) { public AnnualLeave toEntity(Member member){ return AnnualLeave.builder() .annualDateLeave(date) .member(member) .build(); } }public record GetRemainAnnualLeavesResponse(long remainAnnualLeaves) { }💡 @Future 어노테이션을 사용하여 연차를 과거로 떠나려는 시도를 막았습니다. 😊 Commutepublic record GetCommuteRecordRequest(@NotNull long id, @DateTimeFormat(pattern = "yyyy-MM") YearMonth yearMonth) { public int getYear(){ return this.yearMonth.getYear();} public int getMonth(){ return this.yearMonth.getMonth().getValue(); } }@Builder public record GetCommuteDetail(LocalDate date, long workingMinutes, boolean usingDayOff) { public static GetCommuteDetail from(Commute commute){ Duration duration = Duration.between(commute.getCreatedAt(), commute.getUpdatedAt()); return GetCommuteDetail.builder() .date(commute.getCreatedAt().toLocalDate()) .workingMinutes(duration.toMinutes()) .usingDayOff(false) .build(); } public static GetCommuteDetail from(AnnualLeave annualLeave){ return GetCommuteDetail.builder() .date(annualLeave.getAnnualDateLeave()) .workingMinutes(0) .usingDayOff(true) .build(); } } 💡 특정 직원의 날짜별 근무시간 조회시, 범위 내 연차 사용기록이 있으면 CommuteResponseDTO로변환하기위해 GetCommuteDetail를 수정하였고,좀더 쉽게 연도와 월 값을 구하기 위해 requestDTO에서 요청하는 날의 값을 가져올 수 있게GetCommuteRecordRequest를 수정하였습니다.Domain@Getter @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) public class AnnualLeave { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; private LocalDate annualDateLeave; @ManyToOne(fetch = FetchType.LAZY) private Member member; @Builder public AnnualLeave(LocalDate annualDateLeave, Member member) { this.annualDateLeave = annualDateLeave; this.member = member; } }Service@Service @Slf4j @RequiredArgsConstructor public class AnnualLeaveService { private final AnnualLeaveRepository annualLeaveRepository; private final MemberService memberService; @Transactional public void registerAnnualLeave(RegisterAnnualLeaveRequest request){ Member member = memberService.findMemberById(request.id()); if(isAcceptTeamPolicy(member, request)) throw new AcceptTeamPolicyException(); if(isAlreadyUsingAnnualLeaves(member, request.date())) throw new AlreadyRegisteredException(); if(isRemainAnnualLeaves(member)) throw new RemainAnnualLeavesException(); annualLeaveRepository.save(request.toEntity(member)); } @Transactional(readOnly = true) public long getRemainAnnualLeaves(GetRemainAnnualLeavesRequest request){ Member member = memberService.findMemberById(request.id()); return remainAnnualLeaves(member); } private boolean isAcceptTeamPolicy(Member member, RegisterAnnualLeaveRequest request){ return ChronoUnit.DAYS.between(LocalDate.now(), request.date()) = 1 ? 15L : 11L; long usedThisYear = annualLeaveRepository.countByMemberId(member.getId(), YearMonth.now().getYear()); return maxAnnualLeave - usedThisYear; } private boolean isRemainAnnualLeaves(Member member){ return remainAnnualLeaves(member) findAnnualLeavesByMemberIdAndYearMonth(long memberId, YearMonth request){ int year = request.getYear(); int month = request.getMonth().getValue(); // return annualLeaveRepository.findAllAnnualLeavesByMemberIdAndYearMonth(memberId, year, month); } }@Service @Slf4j @RequiredArgsConstructor public class CommuteService { // @@생략 @Transactional(readOnly = true) public GetCommuteRecordResponse GetCommuteRecord(GetCommuteRecordRequest request) { memberService.findMemberById(request.id()); List commuteDetailList = findCommuteListByMemberIdAndStartOfWork(request); long sum = commuteDetailList.stream() .mapToLong(GetCommuteDetail::workingMinutes) .sum(); //commuteDetailList에서 workingMinutes를 조회, reduce로 합을 반환 return new GetCommuteRecordResponse(commuteDetailList, sum); } private List findCommuteListByMemberIdAndStartOfWork(GetCommuteRecordRequest request) { List commuteList = commuteRepository .findCommuteListByMemberIdAndStartOfWork(request.id(), request.getYear(), request.getMonth()); if (commuteList.isEmpty()) throw new CommuteNotFoundException(); //해당범위에 통근기록 존재 X? -> 통근기록없음 예외처리 List commuteDetailList = commuteList.stream() .map(GetCommuteDetail::from) .collect(Collectors.toList()); //통근기록을 CommuteDetail으로 변환 List annualLeaveLeavesList = annualLeaveService .findAnnualLeavesByMemberIdAndYearMonth(request.id(), request.yearMonth()); // 연차기록찾기 (오늘보다 미래의 연차기록은 가져오지않음) mergeAndSort(commuteDetailList, annualLeaveLeavesList); //Merge하고 sort함 return commuteDetailList; } private void mergeAndSort(List commuteDetailList, List annualLeaveLeavesList) { if (annualLeaveLeavesList != null) { //해당범위 연차기록이 있으면 Merge List annualLeavesToDetails = annualLeaveLeavesList.stream() .map(GetCommuteDetail::from) .toList(); commuteDetailList.addAll(annualLeavesToDetails); } commuteDetailList.sort(Comparator.comparing(GetCommuteDetail::date)); //있던없던 sort는 함 } } 💡 특정 직원의 날짜별 근무시간 조회시 연차목록을 조회하기 위해 CommuteService에서 AnnualLeaveList를 참조하도록 하였습니다. Repositorypublic interface AnnualLeaveRepository extends JpaRepository { boolean existsByMemberIdAndAnnualDateLeaveEquals(long memberId, LocalDate annualDate); @Query("SELECT COUNT(*) FROM AnnualLeave annual " + "WHERE annual.member.id = :memberId " + "AND FUNCTION('YEAR', annual.annualDateLeave) = :year") long countByMemberId(long memberId, int year); @Query("SELECT annual FROM AnnualLeave annual " + "WHERE annual.member.id = :memberId " + "AND FUNCTION('YEAR', annual.annualDateLeave) = :year " + "AND FUNCTION('MONTH', annual.annualDateLeave) = :month " + "AND annual.annualDateLeave findAllAnnualLeavesByMemberIdAndYearMonth(long memberId, int year, int month); } countByMemberId는 남은 연차를 계산할때 사용합니다. 기본적으로 memberId와 현재년도로조회하여, 올해 사용한 연차의 갯수를 반환합니다. findAllAnnualLeavesByMemberIdAndYearMonth는 memberId, 요청년도, 요청월,로 조회하며,현재 날짜 이전의 연차 사용기록 리스트를 반환합니다.현재 날짜 이전으로 설정하지 않는다면, 03월 08일에 2024-03월 근무기록 조회시,03월 15일에 신청한 연차 기록이 날짜별 근무시간 조회로 반환될 것입니다.뭔가 굉장히 어색하고 만약 내가 서비스 이용자였다면, 굉장히 유저 경험이 좋지 않았을듯 하여 수정하였습니다. 구현 결과연차 신청 📌 MemberId : 2 인 Member의 2024-12-12연차 신청 📌 2024-12-12에 연차등록 완료 연차 조회 📌 Id : 6인 member의 남은 연차 조회Hibernate: select m1_0.id, m1_0.birthday, m1_0.work_start_date, m1_0.name, m1_0.role, m1_0.team_id from member m1_0 where m1_0.id=? Hibernate: select count(*) from annual_leave al1_0 where al1_0.member_id=? and year(al1_0.annual_date_leave)=?📌 전송되는 쿼리문 📌 사용한 연차 수가 5개이지만,입사한지 1년이 지나지 않았기 때문에 남은 연차 : 6 이 반환된걸 확인할 수 있습니다. 특정 직원의 날짜별 근무시간을 조회하는 기능 Version02 📌 2024-03월 근무기록 조회Hibernate: #멤버 검색 select m1_0.id, m1_0.birthday, m1_0.work_start_date, m1_0.name, m1_0.role, m1_0.team_id from member m1_0 where m1_0.id=? Hibernate: #요청 년,월 근무기록 조회 select c1_0.id, c1_0.attendance, c1_0.start_of_work, c1_0.member_id, c1_0.end_of_work from commute c1_0 where c1_0.member_id=? and year(c1_0.start_of_work)=? and month(c1_0.start_of_work)=? Hibernate: #요청 년, 월 연차기록 조회(미래의 연차기록은 조회X) select al1_0.id, al1_0.annual_date_leave, al1_0.member_id from annual_leave al1_0 where al1_0.member_id=? and year(al1_0.annual_date_leave)=? and month(al1_0.annual_date_leave)=? and al1_0.annual_date_leave 📌 전송되는 쿼리문{ "detail": [ { "date": "2024-03-01", "workingMinutes": 867, "usingDayOff": false }, { "date": "2024-03-02", "workingMinutes": 0, "usingDayOff": true }, { "date": "2024-03-03", "workingMinutes": 0, "usingDayOff": true }, { "date": "2024-03-04", "workingMinutes": 0, "usingDayOff": true }, { "date": "2024-03-05", "workingMinutes": 0, "usingDayOff": true }, { "date": "2024-03-06", "workingMinutes": 619, "usingDayOff": false }, { "date": "2024-03-07", "workingMinutes": 685, "usingDayOff": false }, { "date": "2024-03-08", "workingMinutes": 0, "usingDayOff": true } ], "sum": 2171 }📌 연차를 사용했을 경우, usingDayOff : true 반환📌 edge-case 연차를 사용한 직원이 출근하려는 경우 각 팀별 설정 연차 등록일 이전에 연차를 사용하려하는 경우 해당일에 이미 연차를 등록한 경우 과거로 연차를 사용하려 하는 경우 #### 올해의 연차를 모두 사용한 경우코드 리뷰 Step 03피드백 1.ChronoUnit.DAYS.between(LocalDate.now(), request.date()) = 1 ? 15L : 11L; 📌 매직 넘버를 상수로 처리하면 가독성이 더 좋아질 것 같은데, 어떻게 생각하시나요?나의 답변 1.해결 과정 1.long maxAnnualLeave = ChronoUnit.YEARS.between(member.getCreatedAt(), LocalDateTime.now()) >= 1 ? 15L : 11L;안그래도 이 부분의 15L : 11L 가 조금 명확하지 않다고 생각하여 (15L, 11L이 무슨 숫자인지 알 수가 없다.)enum처리를 하려 합니다.@RequiredArgsConstructor public enum JoinDate { OVER_ONE_YEAR(15L), UNDER_ONE_YEAR(11L); private final long maxAnnualLeaves; public long getAnnualLeaves(){return maxAnnualLeaves;} }long maxAnnualLeave = ChronoUnit.YEARS .between(member.getCreatedAt(), LocalDateTime.now()) >= 1 ? JoinDate.OVER_ONE_YEAR.getAnnualLeaves() : JoinDate.UNDER_ONE_YEAR.getAnnualLeaves(); 💡 JoinDate 를 enum으로 생성하고, maxAnuualLeave를 enum으로 처리하였습니다. 피드백 2.//해당범위에 통근기록 존재 X? -> 통근기록없음 예외처리 List commuteDetailList = commuteList.stream() .map(GetCommuteDetail::from) .collect(Collectors.toList()); //통근기록을 CommuteDetail으로 변환📌`Collectors.toList()`와 Stream.toList()의 차이를 아시나요? Collectors.toList()를 사용한 이유가 궁금합니다!!나의 답변 2.해결 과정 2.안그래도 변환 가능한 리스트를 반환하는게 조금 신경 쓰였는데,MergeAndSort 종료 후, Merge가 완료된 CommuteList를 Collections.unmodifiableList()를 통해 불변 List로감싸서 반환하도록 처리하겠습니다.private List findCommuteListByMemberIdAndStartOfWork(GetCommuteRecordRequest request) { List commuteList = commuteRepository .findCommuteListByMemberIdAndStartOfWork(request.id(), request.getYear(), request.getMonth()); if (commuteList.isEmpty()) throw new CommuteNotFoundException(); //해당범위에 통근기록 존재 X? -> 통근기록없음 예외처리 List commuteDetailList = commuteList.stream() .map(GetCommuteDetail::from) .collect(Collectors.toList()); //통근기록을 CommuteDetail으로 변환 List annualLeaveLeavesList = annualLeaveService // 연차기록찾기 (오늘보다 미래의 연차기록은 가져오지않음) .findAnnualLeavesByMemberIdAndYearMonth(request.id(), request.yearMonth()); mergeAndSort(commuteDetailList, annualLeaveLeavesList); // .addAll()을 통한 merge return Collections.unmodifiableList(commuteDetailList); // 불변리스트로 변환 후 반환 💡 return시 commuteDetailList를 Collections.unmodifiableList()로 감싸서 불변List로 만들어 주었습니다.GitHub
스프링
・
백엔드
2024. 03. 03.
0
[인프런 워밍업 클럽 0기 BE] - 두 번째 발걸음
강의 수강 일주일 동안 학습했던 내용을 요약해주세요.2주차(6일차 ~ 14일차)의 학습 내용. 스프링 컨테이너, JPA 사용, 그리고 AWS EC2 서버를 이용한 배포까지 진도를 나갔다. 일주일 간의 학습 내용에 대한 간단한 회고를 작성해 주세요.-> 강의를 들으며 작성한 강의노트를 바탕으로 작성 19~22강. 역할의 분리와 스프링 컨테이너Spring Container (클래스 저장소)-> 데이터소스/JDBCTemplate/환경...-> 관계를 파악하고 자동으로 의존성 설정을 해준다. Spring Bean-> 스프링 컨테이너 안으로 들어간 클래스를 스프링 빈이라고 한다.-> JDBCTemplate도 스프링 빈으로 등록되어있다.-> @Service / @Repository 어노테이션으로 Service와 Repository도 스프링 빈으로 등록할 수 있다. @PrimaryBookMeomeoryRepository 와 BookMySqlRepository 둘 다 @Repository 선언이 있을 경우,스프링 컨테이너도 어떤 레포지토리를 선택해야할지 모른다.그럴때 @Primary 어노테이션 사용하여 우선권을 정해줄 수 있다. @Configuration클래스에 붙이는 어노테이션.@Bean 을 사용할 때 함께 사용해 주어야 한다. @Bean메소드에 붙이는 어노테이션.메소드에서 반환되는 객체를 스프링 빈에 등록한다. @Component주어진 클래스들을 '컴포넌트'로 간주한다.이 클래스들은 스프링 서버가 시작될 때, 자동으로 감지된다.@Repository, @Service, @RestController 등등에도 @Component 처리가 되어있다. -> @Component , 언제 사용할까?1) @Controller, @Service, @Repository 모두 아니고,2) 개발자가 직접 작성한 클래스를 Spring Bean으로 등록할 때사용되기도 한다. 정리• 우리가 개발과정에서 만들게 되는 class 들은 @Service나 @Repository를 사용하는것이 좋다.( @Configuration 과 @Bean을 통해 구현할 순 있다.)• Spring Bean 을 주입받는 방식에는 3가지가 있는데, 1) 생성자를 통한 주입방식 -> 가장 권장 됨2) @Setter 와 @AutoWired를 통한 주입. -> 누군가가 setter를 사용하면 오작동할 수 있다.3) 필드에 직접 @Autowired 사용 -> 테스트를 어렵게 만드는 요인. 23~29강. Spring Data JPA를 사용한 데이터베이스 조작SQL을 직접 작성하는 것의 단점1) 문자열을 작성하기 때문에 실수할 수 있고, 실수를 인지하는 시점이 느리다.-> 자바 문법이 아니라 그저 문자열일 뿐이기 때문에 컴파일 시점에 에러가 발견되는 것이 아니라, 런타임 시점에 알게된다.2) 특정 데이터베이스에 종속적이게 된다.3) 반복 작업이 많아진다. 테이블을 하나 만들때마다 CRUD 쿼리가 항상 필요함.4) 데이터베이스의 테이블과 개ㅑ객체는 패러다임이 다르다. Java Persistence API (JPA)- 자바 진영의 ORM ?Object : 자바의 객체Realational : 관계형 DB의 TableMapping : 둘을 짝찟는다.-> 객체와 관계형 DB의 테이블을 짝지어 데이터를 영구적으로 저장할 수 있도록 정해진 Java진영의 규칙. HIBERNATE 객체와 관계형 DB의 테이블을 짝지어 데이터를 영구적으로 저장할 수 있도록 정해진 Java 진영의 규칙을 구현한 구현체. (내부적으로 JDBCTemplate 사용) @Entity스프링이 해당 객체와 테이블을 같은 것으로 인식한다.@Entity는 반드시 기본 생성자가 필요하다. (public or protected) @Id해당 필드값을 데이터베이스 테이블의 PrimaryKey 로 인식함. @Columnnull이 들어갈 수 있는지 여부, 길이 제한, DB 컬럼 이름과의 매핑 등등을 설정할 수 있다.만약 @Column 어노테이션이 달려있는 변수의 이름이 데이터베이스의 컬럼명과 동일하다면, name 속성은 생략 가능하다. Spring Data JPASpring Data JPA는 복잡한 JPA 코드를 쉽게 사용할 수 있도록 바꿔준다.Spring DATA JPA -> Hibernate(JPA를 구현) -> JdbcTemplate 이런식으로 접근하여 JDBC를 사용하게 된다. 트랜잭션(Transaction)쪼갤 수 없는 업무의 최소단위.ex) 쇼핑몰에서 물건을 주문시, 1) 주문 기록을 저장, 2) 포인트 저장, 3) 결제 기록 저장-> 하나라도 실패하면 전부다 실패처리 영속성 컨텍스트스프링에선 트랜잭션이 시작되면 영속성 컨텍스트가 생겨나고, 트랜잭션이 종료되면 영속성 컨텍스트가 종료된다. 영속성 컨텍스트의 특수능력 4가지[1] 변경감지 : 영속성 컨텍스트 안에서 불러와진 Entity는 명시적으로 save하지 않아도, 변경을 감지해 자동으로 저장된다.[2] 쓰기지연 : DB의 INSERT/UPDATE/DELETE Sql을 바로 날리는 것이 아니라, 트랜잭션이 commit될 때 모아서 한번만 날린다.[3] 1차캐싱 : ID를 기준으로 Entity를 기억해뒀다가, 다시 해당 Entity를 조회해야할 경우, 캐싱된 객체를 반환하고추가적인 Sql문의 생성을 막는다.[4] 지연로딩 : 연결되어 있는 객체를 꼭 필요한 순간에만 가져온다. (불필요한 sql문의 전송을 막는다.) 30~ 36강. 트랜잭션과 영속성 컨텍스트를 이용한 요구사항 구현@ManyToOne/ @OneToMany(N : 1의 연관관계 )ManyToOne (다수) / OneToMany(1)ManyToOne은 단방향으로 쓸 수 있다. (반대쪽에 OneToMany를 붙여주지않아도 괜찮다.) 연관관계의 주인연관관계의 주인 = 데이터베이스의 Table을 보았을 때, 누가 관계의 주도권을 가지고 있는가?-> 다수(ManyToOne)이 One의 Primary_Key를 가지고 있으므로, Many가 주인이다.연관관계의 주인의 값이 설정되어야지만 진정한 데이터가 저장된다. @ManyToMany(N : N의 연관관계)구조가 복잡하고, 테이블이 직관적으로 매핑되지 않아 사용하지 않는 것을 추천한다. (1 : N으로 나눠버리는것 이 편하다.) cascade 한 객체가 저장되거나 삭제될 때, 그 변경이 폭포처럼 흘러 연결되어 있는 객체도 함께 저장되거나 삭제되는 기능.cascade 옵션을 활용하면 저장이나 삭제를 할 때 연관관계에 놓인 테이블까지 함께 저장 또는 삭제가 이루어진다. orphanRemoval연관관계가 끊어진 데이터를 자동으로 제거해준다. 정리연관관계의 장점1) 각자의 역할에 집중하게 된다. (= 응집성) - 서비스 계층의 역할 : 꼭 필요한 경우 서로 다른 도메인끼리 협업을 하게 도와준다. 트랜잭션을 관리한다. 외부 의존성(Spring Bean)등을 관리한다. - 도메인의 역할 : 도메인 객체가 표현하고 이쓴ㄴ 관심사에 대한 로직을 처리한다. 2) 새로운 개발자가 코드를 읽을 때 이해하기 쉬워진다.3) 테스트 코드 작성이 쉬워진다. 연관관계의 단점1) 지나치게 사용하면 성능상의 문제가 생길 수도 있고 도메인 간의 복잡한 연결로 인해 시스템을 파악하기 어려워질 수 있다.2) 너무 얽혀 있으면 A를 수정했을 경우 B C D 까지 영향이 퍼지게 된다. 37~ 42강. 배포 준비배포최종 사용자에게 SW를 전달하는 과정 / 전용 컴퓨터에 우리의 서버를 옮겨 실행하는 것. Profile배포용 서버는 보통 Linux를 사용하게 된다. (운영환경이 달라짐)-> 똑같은 서버 코드를 실행시키지만, 실행될 때 설정을 다르게 하고 싶다..! H2 DB경량 데이터베이스로 , 개발 단계에서 많이 사용되며, 디스크가 아닌 메모리에 데이터를 저장한다.개발 단계에서는 테이블이 자주 변경되므로, 테이블을 신경쓰지 않고 코드에만 집중할 수 있다. Git코드를 쉽게 관리할 수 있게 해주는 버전관리 프로그램 GitHubgit으로 관리되는 프로젝트의 코드가 저장되는 저장소. git으로 관리되는 프로젝트를 gitHub에 올릴 수 있다.-> GitHub에 저장하는 이유1) 컴퓨터의 코드는 모종의 이유로 소실 될 수 있다.2) 배포를 할 때 활용할 수 있다.3) 내 컴퓨터에서 배포용 컴퓨터로 코드를 옮기는데 깃허브를 사용할 수 있다. 정리1) 배포가 무엇인지 이해하고, 배포를 하기 위해 어떤 준비를 해야 하는지 알아 본다.2) 스프링 서버를 실행할 때 DB와 같은 설정들을 코드 변경 없이 제어할 수 있는 방법을 알아본다.3) Git과 GitHub의 차이를 이해하고, Git에 대한 기초적인 사용법을 알아본다.4) AWS의 EC2가 무엇인지 이해하고, AWS를 통해 클라우드 컴퓨터를 빌려본다. 43~48강. AWS와 EC2 배포정리1) EC2에 접속하는 방법을 알아보고, EC2에 접속해 리눅스 명령어를 다뤄보았다.2) 개발한 서버의 배포를 위해 환경 세팅을 리눅스에 진행하였다.3) Foreground와 Background의 차이를 이해하고, Background 서버를 제어한다.4) 도메인 이름을 사용해 사용자가 IP대신 이름으로 접속 할 수 있도록 한다. 49강~ 마무리. Spring Boot 설정 / 버전업 이해하기스프링과 스프링부트의 차이점 1. 간편한 설정 2. 간단한 의존성 관리 3. 강력한 확장성 4. MSA에 적합한 모니터링 MSA란 ? 하나의 거대한 서버를 이용하는 대신 관심사에 맞는 작은 서버들을 잘게 쪼개서 각 관심사에 맞는 부분들만 관리하는 것. 정리1) Build.gradle 의 정의와 플러그인, dependencies에 대해 학습하였다.2) 스프링과 스프링부트의 차이점에 대해 학습하였다.3) application.yml 파일을 분석하며 YAML 문법에 대해 학습하였다.4) Spring Boot 2.7 버전에서 Spring Boot 3.0 버전으로 마이그레이션을 진행해 보았다. 미션 미션을 해결하는 과정을 요약해 주세요. [6일차 과제]스프링 컨테이너에 대하여 학습하고, 기존에 작성했던 Controller 코드를 3단으로 분리를 하는 과제였지만, 이미 분리를 하여 과제를 수행하였기에, 반대로 다시 합쳐보는 과정을 통해 계층 간 분리의 필요성과 클린코드에 대해 다시 한 번 느낄 수 있는 과제였다. [7일차 과제] 6일차 과제에서 만들었던 Fruit 기능들을 JPA를 이용하여 구현하는 과제였다. 분명 강의를 들었을 땐 이해했다고 느꼈던 개념들이, 막상 직접 구현하려하니 생각보다 어려웠고, JPA에 대한 숙련도가 충분하지 못하다는걸 깨닫게 되었다..또한, DataBase의 Column명과 객체의 필드명이 일치하지 않는 문제로 고생을 했었는데, 객체의 필드명이 만약 soldOut 이런식이면 데이터베이스에 매핑될 때는 sold_out이런식으로 언더바가 들어가게 된다는것을 처음알게 되었다. [미니 프로젝트 - 1일차] 팀 등록 기능 / 직원 등록 기능 / 팀 조회 기능 / 직원 조회 기능을 구현하는 과제였다. 사용된 기술 스택은 자바 17 버전Spring Boot 3.x.x버전JPAMySql이다. [코드 리뷰 - 1일차]리뷰를 통해 내가 미처 생각하지 못했던 더 좋은 프로그래밍 방법이나, 앞으로 무엇을 더 학습하면 좋을지 알게 되었다.나름대로 고민했던 부분들도 다른 분들의 피드백을 통해 더 나은 방향으로 나아갈 수 있었다. 양질의 피드백을 남겨주시는 그룹원들이 정말 너무 고맙다 🙇 GitHubVelog
백엔드
・
스프링
2024. 02. 25.
0
[인프런 워밍업 클럽 0기 BE] - 첫 번째 발걸음
강의 수강 일주일 동안 학습했던 내용을 요약해주세요.1주차(1 일차 ~ 5 일차)의 학습 내용. 서버 개발을 시작하기 위한 환경 설정부터 , 네트워크와 API, 데이터 연동에 대해 배웠고, 리팩토링을 통해 클린코드의 중요성에 대해 배웠다. 일주일 간의 학습 내용에 대한 간단한 회고를 작성해 주세요.-> 강의를 들으며 작성한 강의노트를 바탕으로 작성 1 일차 | - 서버 개발을 위한 환경 설정 및 네트워크 기초 Java를 공부하기 전에 알아두면 좋을 것들! #1(JVM, JDK - 유튜브)컴파일 : 컴퓨터는 생각보다 바보다. 그래서 우리가 작성하는 코드를 알아 먹지 못하기 때문에, 컴파일이라는 과정을 통해 바이너리코드(컴퓨터가 이해하는 0과 1)로 변환 해줘야한다.자바는 컴파일러를 거쳐가기 때문에 운영체제(윈도우, 리눅스)에 영향을 받지않는다.= 다른환경이라도 같은 결과 JVM , JRE, JDKJVM : 자바 가상머신 (운영체제별로 존재)JRE : 자바 실행 환경(JVM이 실행되기 위한 여러 라이브러리)JDK : 자바 개발 도구 (JRE+개발을 위한 도구) -> JDK는 버전이 있고 각 버전별로 새로운 기능이 추가되거나 기존 기능이 사라진다.-> LTS란 다른 버전은 시간이 오래 지나면 지원하지 않는데, 오랫동안 지원해주는 버전을 말한다. Java를 공부하기 전에 알아두면 좋을 것들! #2(빌드, 빌드툴 - 유튜브) 빌드 소스코드파일을 컴퓨터에서 실행할 수 있는 독립 SW가공물로 변환시키는 과정 (독립 SW가공물 = Artifact) 빌드의 세분화 1. 소스코드 컴파일2. 테스트코드 컴파일3. 테스트코드 실행4. 테스트코드 리포트작성5. 기타 추가설정 작업진행6. 패키징 수행 -> 패키징 수행 : 오픈API들과 나의 코드를 하나로 묶는다.7. 최종SW결과물(Artifact) 실행 내가 작성한 코드(테스트코드)를 컴파일을 거쳐 작동시켜 보는것 독립 SW가공물(Articfact)가 나올수도 있고, 나오지 않을수도 있다.인터프리터 언어(자바스크립트, 파이썬) 은 컴파일이 필요없다. 빌드 툴 1. 소스코드의 빌드 과정을 자동으로 처리해주는 프로그램2. 외부소스코드(외부라이브러리) 자동추가, 관리 정리1. 빌드란 단순히 실행하는것과는 다르다.2. 빌드 과정 자동화와 외부 라이브러리 관리를 위해 빌드툴이 사용된다. 1강. 스프링 프로젝트를 시작하는 두 번째 방법정리스프링 프로젝트를 설정해 시작하고 실행하는 방법과 서버란 무엇인지, 네트워크와 HTTP, API는 무엇인지JSON은 무엇인지 등등 서버 개발에 필요한 다양한 개념에 대해 배웠다. 2강. @SpringasBootApplication과 서버Annotation마법같은 일을 자동으로 해줌. 예를들어, @SpringBootApplication 실행에 필요한 모든 설정들을 자동으로 해준다. 서버 (기능을 제공하는 프로그램 or 그 프로그램을 실행시키고 있는 컴퓨터)서버란 어떠한 기능을 제공하는 '것'을 의미한다. ex) 회원가입 기능 / 정보 가져오기 기능/ 추천 기능사람대신 컴퓨터가 이런 기능을 수행한다.누군가의 요청이 있어야 기능을 수행한다.ex) 시리야 오늘의 날씨를 알려줘이 요청을 인터넷을 통해 하게된다. 3강. 네트워크란 무엇인가?!정리네트워크와 택배 시스템의 비교를 통해 IP와 도메인 , port에 대해 학습하였다. 4강. HTTP와 API란 무엇인가?정리택배를 보내는 과정과의 비교를 통해 데이터를 주고 받을 때 사용하는 표준(HTTP)과 API에 대해 학습하였다. HTTP(HyperText Transfer Protocol)내놓아라(운송장을 받는 사람에게 요청하는 행위) / 파란집(운송장이 가는 장소)/ 둘째야(운송장을 실제 받는 사람) /포션(운송장을 받는사람에게 원하는 자원)/ 빨강색 2개(자원의 세부조건)과 같이 현실세계에서 데이터를 주고 받는 표준을 의미한다. API(applicatino Programming Interface)정보를 주고 받기 위해서는 서로 정해진 약속을 해야하고, 이 약속을 통해 특정 기능을 수행하는것.ex) GET / portion?color=red&count=2 5강. GET API 개발하고 테스트하기정리덧셈 API를 만들어 보며 GET API를 만들어보며 DTO에 대해 학습하였다. DTO(Data Transfer object)HTTP 쿼리를 받았을때 적절한 객체가 있다면 스프링 부트가 알아서 그 값을 넣어서 전달하게 되는데, 이때 데이터를 전달하기 위한 적절한 객체를 DTO라고 한다. 6강. POST API 개발하고 테스트하기정리POST API를 만드는 과정에서 HTTP Body로 데이터를 받아보며 JSON에 대해 학습하였다. JSON객체 표기법, 즉 무언가를 표현하기 위한 형식 중괄호 안에 "Key": "value" 형식으로 표기한다ex) {"name": "김영훈", "age": 99} 7~9강. 유저 생성 API와 유저 조회 API 개발정리앞서 배웠던 GET API와 POST API를 활용하여 유저 생성 API와 유저 조회 API를 만들어 보았다.문제점현재 유저 정보가 메모리에서만 유지되고 있기 때문에, 서버를 재실행 하면 모든 데이터가 사라지는 문제점이 있다. 10~16강. 생에 최초 Database 조작하기정리1. 디스크와 메모리의 차이를 이해하고, Database의 필요성에 대해 학습하였다.2.MySQL Database를 SQL과 함께 조작하는 방법에 대해 학습하였다.3.스프링 서버를 이용해 Database에 접근하고 데이터를 저장, 조회 , 업데이트 , 삭제하는 방법에 대해 학습하였다.4. API의 예외 상황을 알아보고 예외를 처리해 보았다. 500 Internal server error함수 내에서 함수가 정상종료되지 않고, 그 안에서 예외가 발생하였기 때문에 500 Internal server error 반환 문제점현재 controller 에서 너무 많은 일을 하고있다.1. DB 접근2. create3. read4. update5. delete 17~18강. 역할의 분리정리controller를 리팩토링 해보며 클린코드의 중요성에 대해 학습하였다. 하나의 함수가 너무 많은 기능을 수행하면 안되는 이유1. 동시에 여러명이 수정할 수 없다.2.읽고 이해하기가 어렵다.3.한부분을 수정하더라도 함수 전체에 영향을 미칠 수 있다.4.테스트가 힘들다.5.유지보수성이 떨어진다. updateUser(변환 전)//변환 전의 userUpdate @PutMapping("/user") public void userUpdate(@RequestBody UserUpdateRequest request){ String readsql = "select * from USER WHERE id=?"; boolean userCheck = jdbcTemplate.query(readsql,(rs, rowNum) -> 0, request.getId()).isEmpty(); if(userCheck){ throw new IllegalArgumentException(); } String sql = "UPDATE USER SET name=? WHERE id=?"; jdbcTemplate.update(sql, request.getName(), request.getId()); } updateUser(변환 후)1.api 진입 지점으로 HTTP Body를 객체로 변환한다. (controller)2.유저가 있는지 없는지 예외처리한다. (service)3.sql을 사용해 실제 database와의 통신을 담당한다. (repository)@PutMapping("/user") public void updateUser(@RequestBody UserUpdateRequest request){ service.updateUser(request); }//service public void updateUser(UserUpdateRequest request) { if (repository.isUserNotExist(request.getId())) throw new IllegalArgumentException(); repository.updateUser(request); }//repository public void updateUser(UserUpdateRequest request) { String sql = "UPDATE USER SET name=? WHERE id=?"; jdbcTemplate.update(sql, request.getName(), request.getId()); } public boolean isUserNotExist(long id) { String sql = "SELECT * FROM user WHERE id=?"; return jdbcTemplate.query(sql, (rs, rowNum) -> 0, id).isEmpty(); } 미션 미션을 해결하는 과정을 요약해 주세요. [1일차 과제](https://velog.io/@vosxja1/%EC%9D%B8%ED%94%84%EB%9F%B0-%EC%9B%8C%EB%B0%8D%EC%97%85-%ED%81%B4%EB%9F%BD-0%EA%B8%B0-BE-1%EC%9D%BC%EC%B0%A8-%EA%B3%BC%EC%A0%9C) 어노테이션을 사용하는 이유를 정리하고 나만의 어노테이션을 만드는 방법을 고민하는 과제였다.무엇을 만들어 볼까 고민하다 어노테이션을 통해 인자를 전달받고, 간단한 연산을 수행하는 계산기를 만들었는데 과제의 의도와 약간 미스매치였지 않나 하는 후회가 남는다. 다른 러너분들중에 @validated 어노테이션을 커스텀 어노테이션을 사용하여 맛깔나게 쓰신분이 계시던데 아마 그것이 과제의 의도와 좀 더 부합되는 활용법이 아닌가 싶었다.조만간 validated 어노테이션과 커스텀 어노테이션에 대해 학습하고 정리해보아야겠다. [2일차 과제](https://velog.io/@vosxja1/%EC%9D%B8%ED%94%84%EB%9F%B0-%EC%9B%8C%EB%B0%8D%EC%97%85-%ED%81%B4%EB%9F%BD-0%EA%B8%B0-BE-2%EC%9D%BC%EC%B0%A8-%EA%B3%BC%EC%A0%9C)GET API와 POST API에 대해 학습하고 , 약간의 응용이 필요했던 과제이다.@GetMapping("/api/day-of-the-week") //@DateTimeFormat으로 yyyy-MM-dd 형태로 변환해서 받는다 public ResponseEntity dateResponse(@RequestParam("date") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate localDate) { DayOfWeek dayOfWeek = apiService.dayOfWeekService(localDate); DayOfWeekResponse dayOfWeekResponse = new DayOfWeekResponse(dayOfWeek); return ResponseEntity.ok() .body(dayOfWeekResponse); }이 당시, @RequestParam을 통해 넘어오는 date를 @DateTimeFormat를 통해 패턴화 시켜 받았는데, 질의응답시간에 springBoot 버전에따라 @DateTimeFormat을 사용하지 않아도 받을 수 있다는 것을 알게 되었다. [3일차 과제](https://velog.io/@vosxja1/%EC%9D%B8%ED%94%84%EB%9F%B0-%EC%9B%8C%EB%B0%8D%EC%97%85-%ED%81%B4%EB%9F%BD-0%EA%B8%B0-BE-3%EC%9D%BC%EC%B0%A8-%EA%B3%BC%EC%A0%9C)자바의 람다식이 왜 등장하였고 람다식과 익명 클래스가 어떤 관계가 있을지 정리하는 과제였다.람다식을 사용하면서도 람다식이 어째서 등장했는지, 익명 클래스는 무엇이고 함수형 인터페이스가 무엇인지도모르면서 사용하고 있었다는걸 깨달았다 ㅠ [4일차 과제](https://velog.io/@vosxja1/%EC%9D%B8%ED%94%84%EB%9F%B0-%EC%9B%8C%EB%B0%8D%EC%97%85-%ED%81%B4%EB%9F%BD-0%EA%B8%B0-BE-4%EC%9D%BC%EC%B0%A8-%EA%B3%BC%EC%A0%9C) 2일차 과제와 유사한 API 생성 문제였다. AWS에서 database의 대소문자 구분 때문에 고생했던날이었다. [5일차 과제](https://velog.io/@vosxja1/%EC%9D%B8%ED%94%84%EB%9F%B0-%EC%9B%8C%EB%B0%8D%EC%97%85-%ED%81%B4%EB%9F%BD-0%EA%B8%B0-BE-5%EC%9D%BC%EC%B0%A8-%EA%B3%BC%EC%A0%9C) 클린하지 않은 코드를 클린하게 리팩토링해보는 과제였다. 이 정도면 나쁘지 않은거 같은데 싶었지만, 다른 러너분들을 보니 아직도 많이 부족하구나.. 싶었다.다음주 금요일에 태현님이 해당 코드 리팩토링을 진행하신다던데 벌써 기대가 된다.
백엔드
・
스프링