diff --git a/.gitignore b/.gitignore index bd2d96c3..53a4c885 100644 --- a/.gitignore +++ b/.gitignore @@ -74,3 +74,8 @@ secrets.yml application.yml jwt.yml +application-dev.yml +application-local.yml +.env.dev + + diff --git a/.sdkmanrc b/.sdkmanrc new file mode 100644 index 00000000..ca5df489 --- /dev/null +++ b/.sdkmanrc @@ -0,0 +1 @@ +java=17.0.8-oracle diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..078d0c29 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,4 @@ +FROM openjdk:17 +ARG JAR_FILE=/build/libs/*.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["java","-jar", "/app.jar"] diff --git a/README.md b/README.md index 19216e47..4c00e550 100644 --- a/README.md +++ b/README.md @@ -1619,4 +1619,280 @@ HTTP 요청의 헤더에 Bearer 토큰 등록 ![Untitled](ceos_4주차_img/Untitled%209.png) -response 정상 응답 \ No newline at end of file +response 정상 응답 + +# 🌱 5주차 미션 + +# 1️⃣ 로컬에서 도커 실행해보기 + +## 1. jar 빌드 + +`./gradlew clean build` + +## 2. Dockerfile + +```docker +FROM openjdk:17 +ARG JAR_FILE=/build/libs/*.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["java","-jar", "/app.jar"] +``` + +## 3. docker-compose 와 환경변수 설정 + +### dev 환경 + +`docker-compose-dev.yml` + +dev profile : web 과 db 컨테이너를 모두 띄움 + +```yaml +version: "3.7" +services: + db: + image: mysql + container_name: spring-daangn-db + env_file: + - .env.dev + ports: + - "3307:3306" + volumes: + - ./dockerDB:/app + restart: always + web: + container_name: spring-daangn-server + build: + context: . + dockerfile: Dockerfile + ports: + - "8082:8080" + environment: + - SPRING_PROFILES_ACTIVE=dev + depends_on: + - db + restart: always +``` + +`application-dev.yml` + +```yaml +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://db:3306/daangn?serverTimezone=Asia/Seoul + username: root + password: 1234 +``` + +### local 환경 + +`docker-compose-local.yml` + +local profile : web 만 띄우고, db 는 로컬db 에 붙음 + +```yaml +version: "3.7" +services: + web: + container_name: spring-daangn-server + build: + context: . + dockerfile: Dockerfile + ports: + - "8082:8080" + environment: + - SPRING_PROFILES_ACTIVE=local +``` + +- SPRING_PROFILES_ACTIVE 로 local profile 주입 + +`application-local.yml` + +```yaml +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://host.docker.internal:3306/ceos_18?serverTimezone=Asia/Seoul + username: root + password: +``` + +- host 명이 `localhost` 가 아니라 `host.docker.internal` 임에 주의 (컨테이너 관점으로 생각) + +1. 빌드 후 컨테이너 띄우기 + + `./gradlew clean build` + + local : `docker-compose -f docker-compose-local.yml up --build` + + dev : `docker-compose -f docker-compose-dev.yml up --build` + + +# 트러블 슈팅 + +## 1. 자바 버전으로 인한 빌드 에러 + +인텔리제이 IDE 에서 빌드를 하는 것이 아니라 터미널 상에서 `./gradlew clean build` 커맨드로 빌드를 시도하였을 때 에러가 발생하였음 + +### 원인 : 자바 버전 문제 + +스프링부트3는 자바17 기반인데 현재 로컬에 설정된 자바는 자바11이였음 + +IDE 에서는 JDK17 을 설정해놨기 때문에 그동안 문제가 없었는데 터미널로 jar 를 빌드,실행할때는 문제가 발생 + +![Untitled](ceos_5주차_img/Untitled.png) + +![Untitled](ceos_5주차_img/Untitled%201.png) + +### 해결방법 + +- 로컬에 설치돼있는 다른 자바 버전 확인 + + `/usr/libexec/java_home -V` + + +![Untitled](ceos_5주차_img/Untitled%202.png) + +- JAVA_HOME 환경변수 변경 + + ```bash + export JAVA_HOME=$(/usr/libexec/java_home -v 17) + ``` + + +그런데 이렇게 하면 자바변경이 필요할 때마다 매번 환경변수를 바꿔줘야 하므로 번거로움 + +⇒ 자바 버전 변경 툴을 사용 + +- node 진영의 nvm 이나, python 의 pyenv 처럼 로컬의 자바버전 변경을 위한 툴 + - SDKMAN + - jEnv + - Jabba + + +SDKMAN 을 사용 + +1. SDKMAN 설치 및 초기화 + + + ```bash + curl -s "https://get.sdkman.io" | bash + ``` + + ```bash + source "$HOME/.sdkman/bin/sdkman-init.sh" + ``` + + +1. **원하는 자바 버전 설치:** + 사용 가능한 자바 버전 목록을 확인 + + ```bash + sdk list java + ``` + + 그런 다음 원하는 버전을 선택하여 설치 + + ```bash + sdk install java 17.0.8-oracle + ``` + + +1. 자바 버전 적용 + - 전역 설정 + + ```bash + sdk default java 17.0.8-oracle + ``` + + - 쉘 설정 (쉘 나가면 버전 해제) + + ```bash + sdk use java 17.0.8-oracle + ``` + + - 프로젝트별 설정 (프로젝트 루트에 .sdkmanrc 파일) + + ```bash + echo "java=17.0.8-oracle" > .sdkmanrc + ``` + + +자바버전이 정상적으로 자바17 로 변경됨 + +![Untitled](ceos_5주차_img/Untitled%203.png) + +빌드도 정상적으로 수행 + +![Untitled](ceos_5주차_img/Untitled%204.png) + +## 2. web 컨테이너가 db 컨테이너에 못 붙는 이슈 + +```yaml +version: "3.7" +services: + db: + image: mysql + container_name: spring-daangn-db + env_file: + - .env.dev + ports: + - "3307:3306" + volumes: + - ./dockerDB:/app + restart: always + web: + container_name: spring-daangn-server + build: + context: . + dockerfile: Dockerfile + ports: + - "8082:8080" + environment: + - SPRING_PROFILES_ACTIVE=dev + depends_on: + - db + restart: always +``` + +depends_on 을 붙혔다고 해서 db 가 완전히 띄워진 다음에 web 컨테이너가 실행되는게 아니라 + +그냥 실행순서만 지정해주는 역할이라서 web 컨테이너에 `restart: always` 를 붙혀야 함 + +## 3. application profile 을 못 읽는 이슈 + +springboot 2.4 이후로 profile 지정 방법이 달라졌다고 함 + +`application-local.yml` + +```yaml +spring: + profiles: # 에러발생 + active: local # 에러발생 + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://host.docker.internal:3306/ceos_18?serverTimezone=Asia/Seoul + username: root + password: + + jpa: + hibernate: + ddl-auto: create + properties: + hibernate: + default_batch_fetch_size: 100 + open-in-view: false +``` + +기존에는 yaml 파일 안에서 spring:profile:active 에서 원하는 profile 명을 써줬어야 하는데 + +springboot 2.4 이후로는 이렇게 하면 에러가 발생함 + +profile:active 부분을 지우고 application-{profile}.yml 로 파일이름만 지정하면 원하는 profile 을 잘 읽어옴 + +> 참고 +> + +spring 에서 application profile 을 읽는 순서 + +⇒ 기본 application.yml 을 먼저 읽고 지정한 application-{profile}.yml 을 그 위에 덮어 씌우는 방식으로 실행 (override 방식) diff --git "a/ceos_5\354\243\274\354\260\250_img/Untitled 1.png" "b/ceos_5\354\243\274\354\260\250_img/Untitled 1.png" new file mode 100644 index 00000000..8378af82 Binary files /dev/null and "b/ceos_5\354\243\274\354\260\250_img/Untitled 1.png" differ diff --git "a/ceos_5\354\243\274\354\260\250_img/Untitled 2.png" "b/ceos_5\354\243\274\354\260\250_img/Untitled 2.png" new file mode 100644 index 00000000..36e4935b Binary files /dev/null and "b/ceos_5\354\243\274\354\260\250_img/Untitled 2.png" differ diff --git "a/ceos_5\354\243\274\354\260\250_img/Untitled 3.png" "b/ceos_5\354\243\274\354\260\250_img/Untitled 3.png" new file mode 100644 index 00000000..d3a8445b Binary files /dev/null and "b/ceos_5\354\243\274\354\260\250_img/Untitled 3.png" differ diff --git "a/ceos_5\354\243\274\354\260\250_img/Untitled 4.png" "b/ceos_5\354\243\274\354\260\250_img/Untitled 4.png" new file mode 100644 index 00000000..ac5252dd Binary files /dev/null and "b/ceos_5\354\243\274\354\260\250_img/Untitled 4.png" differ diff --git "a/ceos_5\354\243\274\354\260\250_img/Untitled.png" "b/ceos_5\354\243\274\354\260\250_img/Untitled.png" new file mode 100644 index 00000000..04802788 Binary files /dev/null and "b/ceos_5\354\243\274\354\260\250_img/Untitled.png" differ diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml new file mode 100644 index 00000000..9a8a443e --- /dev/null +++ b/docker-compose-dev.yml @@ -0,0 +1,25 @@ +version: "3.7" +services: + db: + image: mysql + container_name: spring-daangn-db + env_file: + - .env.dev + ports: + - "3307:3306" + volumes: + - ./dockerDB:/app + restart: always + web: + container_name: spring-daangn-server + build: + context: . + dockerfile: Dockerfile + ports: + - "8082:8080" + environment: + - SPRING_PROFILES_ACTIVE=dev + depends_on: + - db + restart: always + diff --git a/docker-compose-local.yml b/docker-compose-local.yml new file mode 100644 index 00000000..82074a64 --- /dev/null +++ b/docker-compose-local.yml @@ -0,0 +1,11 @@ +version: "3.7" +services: + web: + container_name: spring-daangn-server + build: + context: . + dockerfile: Dockerfile + ports: + - "8082:8080" + environment: + - SPRING_PROFILES_ACTIVE=local diff --git a/src/main/java/com/ceos18/springboot/Application.java b/src/main/java/com/ceos18/springboot/Application.java index 62ca6929..86c31807 100644 --- a/src/main/java/com/ceos18/springboot/Application.java +++ b/src/main/java/com/ceos18/springboot/Application.java @@ -5,6 +5,8 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.PropertySource; +import org.springframework.core.env.Environment; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import java.util.Arrays; @@ -13,8 +15,17 @@ @EnableJpaAuditing public class Application { + + public static void main(String[] args) { - SpringApplication.run(Application.class, args); +// SpringApplication.run(Application.class, args); + + ApplicationContext context = SpringApplication.run(Application.class, args); + Environment env = context.getEnvironment(); + + String jdbcUrl = env.getProperty("spring.datasource.url"); + + System.out.println("JDBC URL : " + jdbcUrl); } // @Bean diff --git a/src/main/java/com/ceos18/springboot/common/ApiResponse.java b/src/main/java/com/ceos18/springboot/common/ApiResponse.java new file mode 100644 index 00000000..4576213b --- /dev/null +++ b/src/main/java/com/ceos18/springboot/common/ApiResponse.java @@ -0,0 +1,60 @@ +package com.ceos18.springboot.common; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ApiResponse { + + private static final String SUCCESS_STATUS = "success"; + private static final String FAIL_STATUS = "fail"; + private static final String ERROR_STATUS = "error"; + + private String status; + private T data; + private String message; + + public static ApiResponse createSuccess(T data) { + return new ApiResponse<>(SUCCESS_STATUS, data, null); + } + + public static ApiResponse createSuccessWithNoContent() { + return new ApiResponse<>(SUCCESS_STATUS, null, null); + } + + // Hibernate Validator에 의해 유효하지 않은 데이터로 인해 API 호출이 거부될때 반환 + public static ApiResponse createFail(BindingResult bindingResult) { + Map errors = new HashMap<>(); + + List allErrors = bindingResult.getAllErrors(); + for (ObjectError error : allErrors) { + if (error instanceof FieldError) { + errors.put(((FieldError) error).getField(), error.getDefaultMessage()); + } else { + errors.put( error.getObjectName(), error.getDefaultMessage()); + } + } + return new ApiResponse<>(FAIL_STATUS, errors, null); + } + + // 예외 발생으로 API 호출 실패시 반환 + public static ApiResponse createError(String message) { + return new ApiResponse<>(ERROR_STATUS, null, message); + } + + private ApiResponse(String status, T data, String message) { + this.status = status; + this.data = data; + this.message = message; + } +} \ No newline at end of file diff --git a/src/main/java/com/ceos18/springboot/common/UserExceptionHandler.java b/src/main/java/com/ceos18/springboot/common/UserExceptionHandler.java new file mode 100644 index 00000000..a359f270 --- /dev/null +++ b/src/main/java/com/ceos18/springboot/common/UserExceptionHandler.java @@ -0,0 +1,17 @@ +package com.ceos18.springboot.common; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class UserExceptionHandler { + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidationExceptions(BindingResult bindingResult) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResponse.createFail(bindingResult)); + } +} \ No newline at end of file diff --git a/src/main/java/com/ceos18/springboot/controller/PostApiController.java b/src/main/java/com/ceos18/springboot/controller/PostApiController.java index 5d15e9a5..92756912 100644 --- a/src/main/java/com/ceos18/springboot/controller/PostApiController.java +++ b/src/main/java/com/ceos18/springboot/controller/PostApiController.java @@ -1,5 +1,6 @@ package com.ceos18.springboot.controller; +import com.ceos18.springboot.common.ApiResponse; import com.ceos18.springboot.dto.post.request.PostCreateRequestDto; import com.ceos18.springboot.dto.post.request.PostUpdateRequestDto; import com.ceos18.springboot.dto.post.response.PostReadResponseDto; @@ -19,42 +20,42 @@ public class PostApiController { // 등록 @PostMapping("/post") - public Long createPost(@RequestBody PostCreateRequestDto requestDto) { + public ApiResponse createPost(@RequestBody PostCreateRequestDto requestDto) { // TODO : 나중에 헤더의 토큰에서 member id 뽑아오는 로직 필요 Long memberId = 1L; // 지금은 임시로 member id를 1 로 설정 - return postService.createPost(requestDto, memberId); + return ApiResponse.createSuccess(postService.createPost(requestDto, memberId)); } // 전체 조회 @GetMapping("/post/") - public List findAllPosts() { - return postService.findAllPosts(); + public ApiResponse> findAllPosts() { + return ApiResponse.createSuccess(postService.findAllPosts()); } // 단건 조회 @GetMapping("/post/{postId}") - public PostReadResponseDto findPost(@PathVariable("postId") Long postId) { - return postService.findPost(postId); + public ApiResponse findPost(@PathVariable("postId") Long postId) { + return ApiResponse.createSuccess(postService.findPost(postId)); } // 수정 @PatchMapping("/post/{postId}") - public Long updatePost(@PathVariable("postId") Long postId, @RequestBody PostUpdateRequestDto requestDto) { + public ApiResponse updatePost(@PathVariable("postId") Long postId, @RequestBody PostUpdateRequestDto requestDto) { Long memberId = 1L; // 지금은 임시로 member id를 1 로 설정 - return postService.updatePost(requestDto, postId, memberId); + return ApiResponse.createSuccess(postService.updatePost(requestDto, postId, memberId)); } //삭제 @DeleteMapping("/post/{postId}") - public Long deletePost(@PathVariable("postId") Long postId) { + public ApiResponse deletePost(@PathVariable("postId") Long postId) { Long memberId = 1L; // 지금은 임시로 member id를 1 로 설정 - return postService.deletePost(postId, memberId); + return ApiResponse.createSuccess(postService.deletePost(postId, memberId)); } } diff --git a/src/main/java/com/ceos18/springboot/controller/SignController.java b/src/main/java/com/ceos18/springboot/controller/SignController.java index 64f330ad..b6c75920 100644 --- a/src/main/java/com/ceos18/springboot/controller/SignController.java +++ b/src/main/java/com/ceos18/springboot/controller/SignController.java @@ -1,5 +1,6 @@ package com.ceos18.springboot.controller; +import com.ceos18.springboot.common.ApiResponse; import com.ceos18.springboot.dto.signIn.request.SignInRequestDto; import com.ceos18.springboot.dto.signIn.response.SignInResponseDto; import com.ceos18.springboot.dto.signUp.request.SignUpRequestDto; @@ -22,13 +23,13 @@ public class SignController { @Operation(summary = "회원 가입") @PostMapping("/sign-up") - public SignUpResponseDto signUp(@RequestBody SignUpRequestDto request) { - return signService.registMember(request); + public ApiResponse signUp(@RequestBody SignUpRequestDto request) { + return ApiResponse.createSuccess(signService.registMember(request)); } @Operation(summary = "로그인") @PostMapping("/sign-in") - public SignInResponseDto signIn(@RequestBody SignInRequestDto request) { - return signService.signIn(request); + public ApiResponse signIn(@RequestBody SignInRequestDto request) { + return ApiResponse.createSuccess(signService.signIn(request)); } } \ No newline at end of file diff --git a/src/test/java/com/ceos18/springboot/repository/PostRepositoryTest.java b/src/test/java/com/ceos18/springboot/repository/PostRepositoryTest.java deleted file mode 100644 index 634893e2..00000000 --- a/src/test/java/com/ceos18/springboot/repository/PostRepositoryTest.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.ceos18.springboot.repository; - -import com.ceos18.springboot.entity.Category; -import com.ceos18.springboot.entity.Member; -import com.ceos18.springboot.entity.Post; -import com.ceos18.springboot.entity.enums.DealType; -import com.ceos18.springboot.entity.enums.PostStatus; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; - -import java.math.BigDecimal; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; - -@DataJpaTest -public class PostRepositoryTest { - - @Autowired - private TestEntityManager em; - - @Autowired - private PostRepository postRepository; - - @Test - public void testSaveAndFind() { - // Given - - Category category = Category.builder() - .name("도서") - .build(); - - Member member = Member.builder() - .phoneNumber("010-1111-2222") - .isWithdrawal(false) - .mannerRating(BigDecimal.valueOf(36.5)) - .region("서울") - .build(); - - em.persist(category); - em.persist(member); - - - Post post1 = Post.builder() - .category(category) - .seller(member) - .title("title") - .dealType(DealType.SELL) - .isPriceOffer(true) - .status(PostStatus.SALE) - .likedCount(0) - .viewCount(0) - .build(); - - Post post2 = Post.builder() - .category(category) - .seller(member) - .title("title") - .dealType(DealType.SELL) - .isPriceOffer(true) - .status(PostStatus.SALE) - .likedCount(0) - .viewCount(0) - .build(); - - Post post3 = Post.builder() - .category(category) - .seller(member) - .title("title") - .dealType(DealType.SELL) - .isPriceOffer(true) - .status(PostStatus.SALE) - .likedCount(0) - .viewCount(0) - .build(); - - - // When - em.persist(post1); - em.persist(post2); - em.persist(post3); - - - List posts = postRepository.findAll(); - - // Then - assertThat(posts).hasSize(3); - assertThat(posts).contains(post1, post2, post3); - } -} \ No newline at end of file