Skip to content

Commit

Permalink
Merge branch 'Master' into weekly
Browse files Browse the repository at this point in the history
  • Loading branch information
yooonwodyd authored Nov 15, 2024
2 parents 224335f + f048aed commit 0e4a239
Show file tree
Hide file tree
Showing 14 changed files with 175 additions and 1,279 deletions.
71 changes: 55 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,66 @@
1. 검색 - 심규민
2. 로그인 및 회원 가입 + 작가 + 팔로우 - 윤재용
3. 채팅 + 파일 - 박한솔
4. 상품(피드), 좋아요, 감상평 - 주보경
4. 상품(피드), 좋아요, 감상평 - 주보경
5. 인프라(ci/cd, https, 모니터링) - 김동현

## 진행상황
1. ERD 설계에 따라 Entity 구현. 세부 구현사항이 남아있음
2. RULE.md에 각종 프로젝트 규칙 정의
3. 매주 월요일 정기회의
4. 다음주까지 각자 1차 구현 후, 회의를 통해 step 3 조별 멘토링을 통해 추가 피드백을 통해 수정.
## CI/CD 및 인프라 구축
![CI/CD 과정 - GitHub Actions를 활용한 배포 프로세스](https://velog.velcdn.com/images/hyunn/post/986c6af9-b694-44c2-a554-dd51e091fde0/image.png)
### CI/CD 과정 - GitHub Actions를 활용한 배포 프로세스

1. **GitHub Actions 트리거**
- Master 브랜치에 코드가 푸시되면 GitHub Action이 자동으로 동작합니다.
2. **도커 이미지 생성 및 업로드**
- 프로젝트를 빌드하고, 도커 이미지를 생성한 후 도커 허브에 이미지를 업로드합니다.
3. **배포 스크립트 실행**
- EC2-1 서버에 접속하여 배포 스크립트(`deploy.sh`)를 실행합니다.
https://github.com/donghyuun/katecam-infra/blob/main/ec2-dual-zero-downtime-lb/deploy.sh

## 리뷰사항
연휴 이후 다음주 부터 본격적인 개발에 들어가려 합니다. 열심히하겠습니다! 감사합니다.
#### (deploy.sh 스크립트 동작)

1. **백엔드 서버의 포트 검사**
- EC2-1, EC2-2에서 현재 동작 중인 백엔드 서버(도커 컨테이너)가 사용 중인 포트(8080 또는 8081)를 검사합니다.
- 사용하지 않는 포트(8081 또는 8080)를 확인합니다.
2. **새 서버 컨테이너 배포**
- 도커 허브에서 최신 이미지를 PULL하여, EC2-1, EC2-2의 사용 가능한 포트에 새 백엔드 서버(도커 컨테이너)를 실행합니다.
3. **헬스 체크 수행**
- 새로운 서버의 정상 동작을 확인하기 위해 `/actuator/health` 엔드포인트로 헬스 체크를 수행합니다.
- 이 과정에서 DB 연결 상태도 함께 확인합니다.
4. **트래픽 분산 대상 변경 (Blue-Green 배포)**
- 헬스 체크가 성공하면 NGINX 설정을 업데이트하여 트래픽을 새 서버로 분산합니다.
- BLUE-GREEN 방식으로 무중단 배포를 수행합니다.
5. **NGINX 리로드**
- 변경된 NGINX 설정을 적용하기 위해 NGINX를 리로드합니다.
6. **기존 서버 종료 및 삭제**
- 기존에 EC2-1, EC2-2에서 실행 중이던 컨테이너를 종료하고 삭제하여 이전 서버를 정리합니다.

### 인프라 구성 - EC2 서버 구성 및 모니터링

# 5주차 github 코드리뷰 질문
(윤재용)
몇몇 컨트롤러에 대한 E2E 테스트를 작성하였습니다.
처음에는 @WithMockUser 를 사용해서 테스트를 진행하려고 했는데, Header를 검증하다보니 불가능했습니다.
저희의 요구사항이 특정 url이 아니라면 헤더에 토큰이 필요하다보니 사용이 불가능하였기에 JwtTestUtils 클래스를 통해 테스트 유저를 사용하였습니다.
#### EC2 - 1

이런 전체 테스트를 처음 구현하다 보니
현재 작성한 테스트가 E2E 테스트라고 불려도 될 지 잘 모르겠습니다..!
추가로 테스트코드의 개선점이 있을지 궁금합니다.
- **백엔드 서버1** (도커 컨테이너)
- **Nginx** - 80 포트로 들어오는 요청을 백엔드 서버 1, 2에 대해 로드밸런싱
- **MySQL** - 백엔드 서버 1, 2가 참조하는 DB
- **Prometheus** - 백엔드 서버 1, 2의 메트릭 수집
- **Grafana** - Prometheus가 수집한 메트릭 시각화 (URL: http://golden-ratio.duckdns.org:3000/) (admin/admin)
- **Dozzle** - EC2-1, EC2-2의 도커 컨테이너 로그 시각화 (URL: http://golden-ratio.duckdns.org:7070/)

#### EC2 - 2

- **백엔드 서버2** (도커 컨테이너)
- **Dozzle** - (Agent) EC2-2의 도커 컨테이너 로그를 EC2-1의 Dozzle에게 전달

#### 서버 1개일 때 vs 2개일 때(LB 적용) TPS 간단 비교 (Swagger GET 기준)

- TPS 테스트 결과: 두 개의 서버로 로드밸런싱 적용 시 성능이 향상됨

### EC2 인스턴스 생성 - Terraform 이용

1. **Terraform을 이용한 EC2 인스턴스 생성**
- Terraform 스크립트를 사용해 AWS에서 EC2 인스턴스를 생성하고, 관련 리소스(보안 그룹, VPC, 서브넷, 키 페어 등)를 설정하였습니다.

2. **생성 절차**
1. **AWS CLI**를 사용해 AWS 계정과 로컬 환경 연결 후 인증 설정
2. 테라폼 스크립트 작성 후 실행(ec2.tf)
https://github.com/donghyuun/terraform-study/blob/main/ec2.tf
- 리소스 설정이 완료된 EC2 인스턴스가 생성됩니다.
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
@EnableRedisRepositories
public class RedisConfig {

@Value("${spring.redis.host}")
@Value("${spring.data.redis.host}")
private String host;

@Value("${spring.redis.port}")
@Value("${spring.data.redis.port}")
private int port;

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.helpmeCookies.global.config;


import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.helpmeCookies.product.controller.docs.ProductApiDocs;
import com.helpmeCookies.product.dto.ProductImageResponse;
import com.helpmeCookies.product.dto.ProductPage;
import com.helpmeCookies.product.dto.ProductPage.Paging;
import com.helpmeCookies.product.dto.ProductRequest;
import com.helpmeCookies.product.dto.ProductResponse;
import com.helpmeCookies.product.entity.Product;
Expand Down Expand Up @@ -40,51 +41,47 @@ public class ProductController implements ProductApiDocs {
private final ReviewService reviewService;
private final ProductLikeService productLikeService;

@PostMapping("/successTest")
public ResponseEntity<ApiResponse<Void>> saveTest() {
return ResponseEntity.ok(ApiResponse.success(SuccessCode.OK));
}

@PostMapping
public ResponseEntity<Void> saveProduct(@RequestBody ProductRequest productRequest) {
public ResponseEntity<ApiResponse<Void>> saveProduct(@RequestBody ProductRequest productRequest) {
Product product = productService.save(productRequest);
productImageService.saveImages(product.getId(),productRequest.imageUrls());
return ResponseEntity.ok().build();
return ResponseEntity.ok(ApiResponse.success(SuccessCode.OK));
}

@PostMapping("/images")
public ResponseEntity<ProductImageResponse> uploadImages(List<MultipartFile> files) {
public ResponseEntity<ApiResponse<ProductImageResponse>> uploadImages(List<MultipartFile> files) {
List<ImageUpload> responses = productImageService.uploadMultiFiles(files);
return ResponseEntity.ok(new ProductImageResponse(responses.stream().map(ImageUpload::photoUrl).toList()));
return ResponseEntity.ok(ApiResponse.success(SuccessCode.OK,new ProductImageResponse(responses.stream().map(ImageUpload::photoUrl).toList())));
}

@GetMapping("/{productId}")
public ResponseEntity<ProductResponse> getProductInfo(@PathVariable("productId") Long productId) {
public ResponseEntity<ApiResponse<ProductResponse>> getProductInfo(@PathVariable("productId") Long productId) {
Product product = productService.find(productId);
List<String> urls = productImageService.getImages(productId);
return ResponseEntity.ok(ProductResponse.from(product,urls));
return ResponseEntity.ok(ApiResponse.success(SuccessCode.OK,ProductResponse.from(product,urls)));
}

@PutMapping("/{productId}")
public ResponseEntity<Void> editProductInfo(@PathVariable("productId") Long productId,
public ResponseEntity<ApiResponse<Void>> editProductInfo(@PathVariable("productId") Long productId,
@RequestBody ProductRequest productRequest) {
productService.edit(productId, productRequest);
return ResponseEntity.ok().build();
return ResponseEntity.ok(ApiResponse.success(SuccessCode.OK));
}

@PutMapping("/{productId}/images")
public ResponseEntity<Void> editImages(@PathVariable("productId") Long productId, List<MultipartFile> files) {
public ResponseEntity<ApiResponse<Void>> editImages(@PathVariable("productId") Long productId, List<MultipartFile> files) {
productImageService.editImages(productId, files);
List<String> images = productImageService.uploadMultiFiles(files).stream()
.map(ImageUpload::photoUrl).toList();
productService.editThumbnailImage(productId,images);
productImageService.saveImages(productId,images);
return ResponseEntity.ok().build();
return ResponseEntity.ok(ApiResponse.success(SuccessCode.OK));
}

@DeleteMapping("/{productId}")
public ResponseEntity<Void> deleteProduct(@PathVariable("productId") Long productId) {
public ResponseEntity<ApiResponse<Void>> deleteProduct(@PathVariable("productId") Long productId) {
productService.delete(productId);
return ResponseEntity.noContent().build();
return ResponseEntity.ok(ApiResponse.success(SuccessCode.NO_CONTENT));
}

@GetMapping
Expand All @@ -105,6 +102,7 @@ public ResponseEntity<ApiResponse<ProductPage.Paging>> getProductsWithRandomPagi
@RequestParam(name = "size", required = false, defaultValue = "20") int size
) {
Pageable pageable = PageRequest.of(0, size);

return ResponseEntity.ok(ApiResponse.success(SuccessCode.OK, productService.getProductsWithRandomPaging(pageable)));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.helpmeCookies.product.controller;

import com.helpmeCookies.global.ApiResponse.ApiResponse;
import com.helpmeCookies.global.ApiResponse.SuccessCode;
import com.helpmeCookies.global.jwt.JwtUser;
import com.helpmeCookies.product.dto.ProductPage.Paging;
import com.helpmeCookies.product.service.ProductLikeService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/v1/wishes")
@RequiredArgsConstructor
public class WishController {
private ProductLikeService productLikeService;

@GetMapping
public ResponseEntity<ApiResponse<Paging>> getAllMyLikeProducts(
@AuthenticationPrincipal JwtUser jwtUser, Pageable pageable) {
return ResponseEntity.ok(ApiResponse.success(SuccessCode.OK,productLikeService.getLikeProducts(jwtUser.getId(),pageable)));
}
}
18 changes: 18 additions & 0 deletions src/main/java/com/helpmeCookies/product/dto/ProductPage.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.helpmeCookies.product.dto;

import com.helpmeCookies.product.entity.Product;
import com.helpmeCookies.product.repository.dto.ProductSearch;
import java.util.List;
import org.springframework.data.domain.Page;
Expand All @@ -23,6 +24,16 @@ public static Info from(ProductSearch productSearch) {
);
}

public static Info fromProduct(Product product) {
return new Info(
product.getId(),
product.getName(),
product.getArtistInfo().getNickname(),
product.getPrice(),
product.getThumbnailUrl()
);
}

public static List<Info> of(List<ProductSearch> content) {
return content.stream()
.map(Info::from)
Expand All @@ -41,6 +52,13 @@ public static Paging from(Page<ProductSearch> productPage) {
Info.of(productPage.getContent())
);
}

public static Paging fromProduct(Page<Product> productPage) {
return new Paging(
productPage.hasNext(),
productPage.map(Info::fromProduct).toList()
);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public record ProductRequest(
List<String> imageUrls

) {
public Product toEntity(ArtistInfo artistInfo) {
public Product toEntity(ArtistInfo artistInfo,String thumbnailImage) {
return Product.builder()
.name(name)
.category(Category.fromString(category))
Expand All @@ -29,6 +29,11 @@ public Product toEntity(ArtistInfo artistInfo) {
.preferredLocation(preferredLocation)
.hashTags(hashTags)
.artistInfo(artistInfo)
.thumbnailUrl(thumbnailImage)
.build();
}

public String getThumbnailImage() {
return imageUrls.get(0);
}
}
4 changes: 4 additions & 0 deletions src/main/java/com/helpmeCookies/product/entity/Like.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,8 @@ public Like(User user, Product product) {
this.user = user;
this.product = product;
}

public Product getProduct() {
return product;
}
}
14 changes: 13 additions & 1 deletion src/main/java/com/helpmeCookies/product/entity/OrderStatus.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
package com.helpmeCookies.product.entity;

public enum OrderStatus {
ORDER, CANCEL
ORDER("핀매 중"),
DONE("거래 완료"),
RESERVED("예약 중");

private final String orderStatusType;

OrderStatus(String orderStatusType) {
this.orderStatusType = orderStatusType;
}

public String getOrderStatusType() {
return orderStatusType;
}
}
7 changes: 6 additions & 1 deletion src/main/java/com/helpmeCookies/product/entity/Product.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,15 @@ public class Product extends BaseTimeEntity {
public Product() {}

@Builder
public Product(String name, Category category, String size, Long price, String description, String preferredLocation, List<HashTag> hashTags, ArtistInfo artistInfo) {
public Product(String name, Category category, String size, Long price, String description, String preferredLocation, List<HashTag> hashTags, ArtistInfo artistInfo,String thumbnailUrl) {
this.name = name;
this.category = category;
this.size = size;
this.price = price;
this.description = description;
this.preferredLocation = preferredLocation;
this.hashTags = hashTags;
this.thumbnailUrl = thumbnailUrl;
this.artistInfo = artistInfo;
}

Expand Down Expand Up @@ -105,6 +106,10 @@ public ArtistInfo getArtistInfo() {
return artistInfo;
}

public String getThumbnailUrl() {
return thumbnailUrl;
}

public void update(String name, Category category, String size, Long price, String description, String preferredLocation, List<HashTag> hashTags, ArtistInfo artistInfo) {
this.name = name;
this.category = category;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@
import com.helpmeCookies.product.entity.Product;
import com.helpmeCookies.user.entity.User;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ProductLikeRepository extends JpaRepository<Like, Long> {
Optional<Like> findDistinctFirstByUserAndProduct(User user, Product product);

Page<Like> findAllByUser(User user, Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package com.helpmeCookies.product.service;

import com.helpmeCookies.product.dto.ProductPage;
import com.helpmeCookies.product.entity.Like;
import com.helpmeCookies.product.entity.Product;
import com.helpmeCookies.product.repository.ProductLikeRepository;
import com.helpmeCookies.product.repository.ProductRepository;
import com.helpmeCookies.user.entity.User;
import com.helpmeCookies.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -19,18 +22,29 @@ public class ProductLikeService {

@Transactional
public void productLike(Long userId, Long productId) {
User user = userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("유효하지 않은 유저Id입니다." + userId));
User user = getUser(userId);
Product product = productRepository.findById(productId).orElseThrow(() -> new IllegalArgumentException("유효하지 않은 상품Id입니다." + productId));
Like like = new Like(user, product);
productLikeRepository.save(like);
}

@Transactional(readOnly = true)
public ProductPage.Paging getLikeProducts(Long userId, Pageable pageable) {
User user = getUser(userId);
Page<Product> productPage = productLikeRepository.findAllByUser(user,pageable).map(Like::getProduct);
return ProductPage.Paging.fromProduct(productPage);
}

@Transactional
public void deleteProductLike(Long userId, Long productId) {
User user = userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("유효하지 않은 유저Id입니다." + userId));
User user = getUser(userId);
Product product = productRepository.findById(productId).orElseThrow(() -> new IllegalArgumentException("유효하지 않은 상품Id입니다." + productId));

Like like = productLikeRepository.findDistinctFirstByUserAndProduct(user,product).orElseThrow(() -> new IllegalArgumentException("존재하지 않는 상품 찜 항목입니다."));
productLikeRepository.delete(like);
}

private User getUser(Long userId) {
return userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("유효하지 않은 유저Id입니다." + userId));
}
}
Loading

0 comments on commit 0e4a239

Please sign in to comment.