


스프링부트의 구조에 맞춰 패키지 구성을 다음과 같이 한다.

Controller를 통해서 외부요청을 받고(API),
Service를 통해서 비즈니스 로직을 만들고(Business Logic),
Repository에서 데이터를 저장한다.(Persistence Logic)
DAO는 데이터에 접근하는 객체이다.
DTO는 계층간 데이터 교환을 위한 객체이다.
Entity는 실제 DB의 테이블과 매칭될 클래스이다.
DAO는 동작위주,Repository는 저장소.
Repository는 단순하게 생각하면 그냥 저장소라고 생각할 수 있다. (객체의 정보를 가진 저장소에 대한 관리 - 데이터베이스에서 데이터 찾기 등)
DAO는 테이블 생성, 업데이트, 삭제 등의 동작 수행.
다시 말해서,
DAO는Service가DB에 연결될 수 있게 해주는 역할을 한다. (Repository를 활용해서 사용하는 것이다 - 접근하는 본질은Repository에 있다.)
DAO는 데이터베이스에 접근하는 객체로,Repository의 메소드를 활용해서DB에 접근해 데이터를 조회하거나 조작하는 기능을 전담한다. (즉, 직접적으로 사용하는 것은DAO이다.)
복잡한 쿼리 로직이나 특정 데이터베이스 기술에 종속적인 기능은 DAO에서 처리하고, 그 결과를 도메인 객체의 컬렉션으로 반환하는 Repository를 통해 비즈니스 로직을 처리할 수 있다.
이렇게 구성하면, DAO는 데이터베이스 기술과 관련된 코드를 캡슐화하고, Repository는 도메인 로직을 캡슐화함으로써, 각 레이어의 책임을 명확히 분리할 수 있다.
간단하게 스프링 부트의 로직에 대해서 살펴보았다.
이제 Spring Boot를 이용해 서버를 만들어보자.
: 먼저, Spring Initializer(https://start.spring.io/)를 사용하거나, IDE(예: IntelliJ, Eclipse)에서 Spring Boot 프로젝트를 생성합니다. 필요한 의존성(Dependencies)를 선택합니다. 예를 들어, 웹 애플리케이션을 만들 경우 Spring Web, 데이터베이스를 사용할 경우 Spring Data JPA, 보안을 적용할 경우 Spring Security 등을 선택합니다.
: 생성된 프로젝트에서 application.properties 혹은 application.yml 파일을 통해 애플리케이션의 설정을 진행합니다. 예를 들어, 데이터베이스 연결 정보, 포트 번호, 로깅 설정 등을 정의합니다.
: 도메인에 해당하는 클래스를 생성합니다. 예를 들어, 사용자 정보를 다루는 서버를 만들 경우 User 클래스를 만들고 필요한 필드를 정의합니다.
만들고자하는 Entity는 StoreEntity이다.
package com.wara.store.Entity;
//import jakarta.persistence.*;
//import jakarta.persistence.Id;
import javax.persistence.*;
import lombok.*;
import java.util.List;
@Setter
@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Builder
public class StoreEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long storeId;
String storeName;
String storePhone;
String storeSeller;
String storeImage;
// @ElementCollection
// @CollectionTable(name = "store_product", joinColumns = @JoinColumn(name = "store_id"))
// @Column(name = "product_id")
// List<Long> productIds;
}
스프링 컨테이너에 등록하기 위해 @Entity 어노테이션을 붙여준다.
@Entity로 선언하게 되면, @Id가 필요하다.
@GeneratedValue(strategy = GenerationType.IDENTITY)를 통해서 엔티티의 기본 키를 자동으로 생성한다.
Lombok을 통해서 @Getter, @Setter, @ToString, @Builder, @NoArgsConstructor, @AllArgsConstructor 등 다양한 어노테이션을 활용해 간단하게 작성할 수 있다.
@ElementCollection
@CollectionTable(name = "store_product", joinColumns = @JoinColumn(name = "store_id"))
@Column(name = "product_id")
List<Long> productIds;
위의 코드는 추후에 store에 해당하는 상품들에 대한 list를 가져오기 위한 코드이다.
: 데이터베이스와의 상호작용을 위해 Repository 인터페이스를 생성합니다. 이 인터페이스는 Spring Data JPA가 제공하는 JpaRepository를 상속받습니다.
package com.wara.store.Repository;
import com.wara.store.Entity.StoreEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface StoreRepository extends JpaRepository<StoreEntity, Long> {
Boolean existsByStoreId(Long storeId); //storeId로 상점 존재 확인
Boolean existsByStoreSeller(String storeSeller); //상점 이름으로 상점 존재 확인
StoreEntity findByStoreId(Long storeId); //storeId는 고유하다.
List<StoreEntity> findByStoreSeller(String storeSeller); //storeSeller는 여러개의 store를 가질 수 있다.
@Query("SELECT se.storeImage FROM StoreEntity se WHERE se.storeId = :storeId")
String findStoreImageByStoreId(@Param("storeId") Long storeId); //storeId로 storeImage를 찾는다.
}
상점의 이미지(storeImage)는 이미지를 담고있는 서버(imageServer)를 따로 만들어, 해당 서버에서 가져올 예정이다. 따라서, @Query를 통해서 조인을 하는 과정이 필요할 것으로 예상이 된다.
JpaRepository를 상속하고 있기 때문에 JpaRepository에서 제공하는 기본 메서드를 사용할 수 있으나, 추가적인 기능이 필요하다면
Custom Query또는@Query어노테이션을 활용해 기능을 구현한다.
다음은 JpaRepository에서 제공하는 기본 메서드이다.
- Save (Create/Update):
- save(T entity): 엔티티를 저장하거나 업데이트합니다.
- Find (Read):
- findById(ID id): 주어진 ID에 해당하는 엔티티를 조회합니다.
- findAll(): 모든 엔티티를 조회합니다.
- Delete:
- delete(T entity): 주어진 엔티티를 삭제합니다.
- deleteById(ID id): 주어진 ID에 해당하는 엔티티를 삭제합니다.
- deleteAll(): 모든 엔티티를 삭제합니다.
- Count:
- count(): 저장된 엔티티의 총 개수를 반환합니다.
- Exists:
- existsById(ID id): 주어진 ID에 해당하는 엔티티가 존재하는지 여부를 확인합니다.
- Flush:
- flush(): 영속성 컨텍스트의 변경을 데이터베이스에 즉시 반영합니다.
: 각 도메인 모델에 대한 데이터베이스 접근 로직을 처리하는 DAO를 생성합니다. DAO는 데이터베이스의 CRUD(Create, Read, Update, Delete) 작업을 수행하며, 특정 데이터베이스 기술에 종속적일 수 있습니다.
확장성과 유지보수성을 고려해 DAO 인터페이스와, DAO 구현체로 나누어 구현을 진행한다.
DAO 인터페이스storeId, storeSeller를 통해 상점이 존재한지 확인storeId, storeSeller를 통해 상점정보 읽어오기storeId로 상점 이미지 읽어오기package com.wara.store.DAO;
import com.wara.store.Entity.StoreEntity;
import org.springframework.stereotype.Repository;
import java.util.Map;
@Repository
public interface StoreDAO{
//상점 존재 여부 Exist
public Map<String, Object> existStoreById(Long storeId);
public Map<String, Object> existStoreBySeller(String storeSeller);
//상점 생성 Create
public Map<String, Object> createStore(StoreEntity storeEntity);
//상점 읽기 Read
public Map<String, Object> readStoreById(Long storeId);
public Map<String, Object> readStoreBySeller(String storeSeller);
public String readStoreImageByStoreId(Long storeId);
//상점 업데이트 Update
public Map<String, Object> updateStoreById(StoreEntity storeEntity);
//상점 삭제 Delete
public Map<String, Object> deleteStore(Long storeId);
// public Boolean deleteProductId(Long storeId, Long productId);
}
DAO 구현체package com.wara.store.DAO;
import com.wara.store.Entity.StoreEntity;
import com.wara.store.Repository.StoreRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Repository
public class StoreDAOImpl implements StoreDAO{
//Alt + Shift + Enter로 StoreDAO interface의 method를 override
private final StoreRepository storeRepository;
public StoreDAOImpl(@Autowired StoreRepository storeRepository) {
this.storeRepository = storeRepository;
}
//Exist
@Override
public Map<String, Object> existStoreById(Long storeId) {
Map<String, Object> result = new HashMap<>();
if (storeRepository.existsByStoreId(storeId)) {
result.put("result", "success");
} else {
result.put("result", "fail");
}
return result;
}
@Override
public Map<String, Object> existStoreBySeller(String storeSeller) {
Map<String, Object> result = new HashMap<>();
if (storeRepository.existsByStoreSeller(storeSeller)) {
result.put("result", "success");
} else {
result.put("result", "fail");
}
return result;
}
//Create
@Override
public Map<String, Object> createStore(StoreEntity storeEntity) {
Map<String, Object> result = new HashMap<>();
storeRepository.save(storeEntity);
if (storeRepository.existsByStoreId(storeEntity.getStoreId())) {
result.put("result", "success");
} else {
result.put("result", "fail");
}
return result;
}
//Read
@Override
public Map<String, Object> readStoreById(Long storeId) {
Map<String, Object> result = new HashMap<>();
StoreEntity storeEntity = storeRepository.findByStoreId(storeId);
if (storeEntity == null) {
result.put("result", "fail");
} else {
result.put("result", "success");
result.put("data", storeEntity);
}
return result;
}
@Override
public Map<String, Object> readStoreBySeller(String storeSeller) {
Map<String, Object> result = new HashMap<>();
List<StoreEntity> storeEntities = storeRepository.findByStoreSeller(storeSeller);
if (storeEntities.isEmpty()) {
result.put("result", "fail");
} else {
result.put("result", "success");
result.put("data", storeEntities);
}
return result;
}
@Override
public String readStoreImageByStoreId(Long storeId) {
String image = storeRepository.findStoreImageByStoreId(storeId);
if(image != null){
return image;
} else {
return "fail";
}
}
//Update
@Override
public Map<String, Object> updateStoreById(StoreEntity storeEntity) {
Map<String, Object> result = new HashMap<>();
StoreEntity oldStoreEntity = storeRepository.findByStoreId(storeEntity.getStoreId());
if (oldStoreEntity != null) {
storeEntity.setStoreId(Optional.ofNullable(storeEntity.getStoreId()).orElse(oldStoreEntity.getStoreId()));
storeEntity.setStoreName(Optional.ofNullable(storeEntity.getStoreName()).orElse(oldStoreEntity.getStoreName()));
storeEntity.setStorePhone(Optional.ofNullable(storeEntity.getStorePhone()).orElse(oldStoreEntity.getStorePhone()));
storeEntity.setStoreSeller(Optional.ofNullable(storeEntity.getStoreSeller()).orElse(oldStoreEntity.getStoreSeller()));
storeEntity.setStoreImage(Optional.ofNullable(storeEntity.getStoreImage()).orElse(oldStoreEntity.getStoreImage()));
storeRepository.save(storeEntity);
result.put("result", "success");
} else {
result.put("result", "fail");
}
return result;
}
//Delete
@Override
public Map<String, Object> deleteStore(Long storeId) {
Map<String, Object> result = new HashMap<>();
StoreEntity storeEntity = storeRepository.findByStoreId(storeId);
if (storeEntity == null) {
result.put("result", "fail");
} else {
storeRepository.delete(storeEntity);
result.put("result", "success");
}
return result;
}
// @Override
// public Boolean deleteProductId(Long storeId, Long productId) {
// StoreEntity storeEntity = storeRepository.findByStoreId(storeId);
//
// if (storeEntity != null) {
// List<Long> productIds = storeEntity.getProductIds();
//
// for (Long oldProductId : productIds) {
// if (productId.equals(oldProductId)) {
// productIds.remove(productId);
// break;
// }
// }
//
// storeEntity.setProductIds(productIds);
//
// storeRepository.save(storeEntity);
// return true;
// } else {
// return false;
// }
// return null;
// }
}
DAO는 Repository의 메소드를 활용해서 DB에 접근해 데이터를 조회하거나 조작하는 기능을 전담한다.
비즈니스 로직에 가까운 것을 볼 수 있다.
: 클라이언트와 서버 간에 데이터를 효과적으로 전송하기 위해 DTO를 생성합니다. DTO는 보통 각 도메인 모델에 대응되며, 클라이언트에게 전달할 데이터를 포함하고 있습니다.
클라이언트에 전달하고자하는 데이터를 생각하면서 구상한다.
상점의 정보인 StoreDTO, 요청에 대한 간단한 응답을 보낼 SimpleResponseDTO, 요청에 대한 응답 형태인 ResponseDTO, 요청에 대한 응답으로 상점 데이터를 넘겨줄 때 사용할 형태인 ReadResponseDTO로 구성하고 진행한다.
package com.wara.store.DTO;
import lombok.*;
import java.util.List;
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class StoreDTO {
Long storeId;
String storeName;
String storePhone;
String storeSeller;
String storeImage;
List<Long> productId;
}
package com.wara.store.DTO;
import lombok.*;
import java.util.List;
@Builder
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class ReadResponseDTO {
Long storeId;
String storeName;
String storePhone;
String storeSeller;
String storeImage;
List<Long> productId;
}
package com.wara.store.DTO;
import lombok.*;
@Builder
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class ResponseDTO {
String result;
Object data;
}
package com.wara.store.DTO;
import lombok.*;
@Builder
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class SimpleResponseDTO {
String result;
}
: 비즈니스 로직을 처리하는 Service 클래스를 생성합니다. 이 클래스에서는 Repository를 사용해 데이터베이스와의 상호작용을 처리합니다.
package com.wara.store.Service;
import com.wara.store.DTO.*;
import com.wara.store.Entity.StoreEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
@Service
public interface StoreService {
//Exist
public SimpleResponseDTO existStoreById(Long storeId);
public SimpleResponseDTO existStoreBySeller(String storeSeller);
//Create
public SimpleResponseDTO createStore(StoreDTO storeDTO);
public SimpleResponseDTO createStore(StoreDTO storeDTO, MultipartFile image);
//Read
public ResponseDTO readStoreById(Long storeId);
public ResponseDTO readStoreBySeller(String storeSeller);
//이미지는 다른 서버에서 가져옴.
//Update
public SimpleResponseDTO updateStoreById(StoreDTO storeDTO);
public SimpleResponseDTO updateStoreById(StoreDTO storeDTO, MultipartFile image);
//Delete
public SimpleResponseDTO deleteStore(Long storeId);
public SimpleResponseDTO deleteProduct(Long storeId, Long productId);
//상품은 다른 서버에서 가져옴
//기타 서비스
public StoreEntity toEntity(StoreDTO storeDTO);
public ResponseDTO toResponseDTO(Map<String, Object> resultMap);
public SimpleResponseDTO toSimpleResponseDTO(Map<String, Object> resultMap);
}
package com.wara.store.Service;
import com.wara.store.DAO.StoreDAO;
import com.wara.store.DTO.ReadResponseDTO;
import com.wara.store.DTO.ResponseDTO;
import com.wara.store.DTO.SimpleResponseDTO;
import com.wara.store.DTO.StoreDTO;
import com.wara.store.Entity.StoreEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service
public class StoreServiceImpl implements StoreService{
private final StoreDAO storeDAO;
private final HttpCommunicationService httpCommunicationService;
//자바프로젝트 진행시 같은 패키지 내에 있는 파일은 import 하지 않아도 돤다.
private final static Logger logger = LoggerFactory.getLogger(StoreServiceImpl.class);
public StoreServiceImpl(@Autowired StoreDAO storeDAO, @Autowired HttpCommunicationService httpCommunicationService
) {
this.storeDAO = storeDAO;
this.httpCommunicationService = httpCommunicationService;
}
//Exist
@Override
public SimpleResponseDTO existStoreById(Long storeId) {
Map<String, Object> resultMap = storeDAO.existStoreById(storeId);
SimpleResponseDTO simpleResponseDTO = toSimpleResponseDTO(resultMap);
return simpleResponseDTO; }
@Override
public SimpleResponseDTO existStoreBySeller(String storeSeller) {
Map<String, Object> resultMap = storeDAO.existStoreBySeller(storeSeller);
SimpleResponseDTO simpleResponseDTO = toSimpleResponseDTO(resultMap);
return simpleResponseDTO;
}
//Create
@Override
public SimpleResponseDTO createStore(StoreDTO storeDTO) {
StoreEntity storeEntity = toEntity(storeDTO);
Map<String, Object> resultMap = storeDAO.createStore(storeEntity);
SimpleResponseDTO simpleResponseDTO = toSimpleResponseDTO(resultMap);
return simpleResponseDTO;
}
@Override
public SimpleResponseDTO createStore(StoreDTO storeDTO, MultipartFile image) {
StoreEntity storeEntity = toEntity(storeDTO);
try {
String imageUri = httpCommunicationService.imageUpload(image);
storeEntity.setStoreImage(imageUri);
Map<String, Object> resultMap = storeDAO.createStore(storeEntity);
SimpleResponseDTO simpleResponseDTO = toSimpleResponseDTO(resultMap);
return simpleResponseDTO;
} catch (URISyntaxException | IOException e) {
e.printStackTrace();
SimpleResponseDTO errorResponse = new SimpleResponseDTO("Failed to upload image");
return errorResponse;
}
}
//Read
@Override
public ResponseDTO readStoreById(Long storeId) {
Map<String, Object> resultMap = storeDAO.readStoreById(storeId);
ResponseDTO responseDTO = toResponseDTO(resultMap);
return responseDTO;
}
@Override
public ResponseDTO readStoreBySeller(String storeSeller) {
Map<String, Object> resultMap = storeDAO.readStoreBySeller(storeSeller);
ResponseDTO responseDTO = toResponseDTO(resultMap);
return responseDTO;
}
//Update
@Override
public SimpleResponseDTO updateStoreById(StoreDTO storeDTO) {
StoreEntity storeEntity = toEntity(storeDTO);
Map<String, Object> existMap = storeDAO.existStoreById(storeDTO.getStoreId());
SimpleResponseDTO simpleResponseDTO;
if (existMap.get("result").equals("success")) {
Map<String, Object> resultMap = storeDAO.updateStoreById(storeEntity);
simpleResponseDTO = toSimpleResponseDTO(resultMap);
} else {
simpleResponseDTO = toSimpleResponseDTO(existMap);
}
return simpleResponseDTO;
}
@Override
public SimpleResponseDTO updateStoreById(StoreDTO storeDTO, MultipartFile image) {
StoreEntity storeEntity = toEntity(storeDTO);
Map<String, Object> existMap = storeDAO.existStoreById(storeEntity.getStoreId());
SimpleResponseDTO simpleResponseDTO;
if (!(existMap.get("result").equals("success"))) {
simpleResponseDTO = toSimpleResponseDTO(existMap);
return simpleResponseDTO;
}
try {
String oldImage = storeDAO.readStoreImageByStoreId(storeEntity.getStoreId());
String imageKey = findImageKey(oldImage);
String imageUri = httpCommunicationService.imageUpdate(image, imageKey);
storeEntity.setStoreImage(imageUri);
Map<String, Object> resultMap = storeDAO.updateStoreById(storeEntity);
simpleResponseDTO = toSimpleResponseDTO(resultMap);
return simpleResponseDTO;
} catch (URISyntaxException | IOException e) {
e.printStackTrace();
SimpleResponseDTO errorResponse = new SimpleResponseDTO("Failed to update image");
return errorResponse;
}
}
//Delete
@Override
public SimpleResponseDTO deleteStore(Long storeId) {
try {
String image = storeDAO.readStoreImageByStoreId(storeId);
String imageKey = findImageKey(image);
Boolean imageDeleteFlag = httpCommunicationService.imageDelete(imageKey);
// Boolean productDeleteFlag = httpCommunicationService.productDelete(storeId);
if (imageDeleteFlag
// && productDeleteFlag
) {
Map<String, Object> resultMap = storeDAO.deleteStore(storeId);
SimpleResponseDTO simpleResponseDTO = toSimpleResponseDTO(resultMap);
return simpleResponseDTO;
} else {
SimpleResponseDTO errorResponse = new SimpleResponseDTO("Failed to Delete image");
return errorResponse;
}
} catch (URISyntaxException e) {
e.printStackTrace();
SimpleResponseDTO errorResponse = new SimpleResponseDTO("Failed to Delete image");
return errorResponse;
}
}
// @Override
// public SimpleResponseDTO deleteProduct(Long storeId, Long productId) {
// Boolean flag = storeDAO.deleteProductId(storeId, productId);
//
// if(flag){
// return new SimpleResponseDTO("success");
// }
//
// return new SimpleResponseDTO("fail");
// }
//기타 서비스
@Override
public StoreEntity toEntity(StoreDTO storeDTO) {
StoreEntity storeEntity = StoreEntity.builder()
.storeId(storeDTO.getStoreId())
.storeName(storeDTO.getStoreName())
.storeSeller(storeDTO.getStoreSeller())
.storePhone(storeDTO.getStorePhone())
.storeImage(storeDTO.getStoreImage())
// .productIds(storeDTO.getProductId())
.build();
return storeEntity;
}
@Override
public ResponseDTO toResponseDTO(Map<String, Object> resultMap) {
ResponseDTO responseDTO;
if (resultMap.containsKey("data")) {
StoreEntity storeEntity = (StoreEntity) resultMap.get("data");
ReadResponseDTO readResponseDTO = ReadResponseDTO.builder()
.storeId(storeEntity.getStoreId())
.storeName(storeEntity.getStoreName())
.storeSeller(storeEntity.getStoreSeller())
.storePhone(storeEntity.getStorePhone())
.storeImage(storeEntity.getStoreImage())
// .productId(storeEntity.getProductIds())
.build();
responseDTO = ResponseDTO.builder()
.result((String) resultMap.get("result"))
.data(readResponseDTO)
.build();
} else if (resultMap.containsKey("dataList")) {
List<StoreEntity> storeEntities = (List<StoreEntity>) resultMap.get("dataList");
List<ReadResponseDTO> readResponseDTOS = new ArrayList<>();
for (StoreEntity storeEntity : storeEntities) {
ReadResponseDTO readResponseDTO = ReadResponseDTO.builder()
.storeId(storeEntity.getStoreId())
.storeName(storeEntity.getStoreName())
.storeSeller(storeEntity.getStoreSeller())
.storePhone(storeEntity.getStorePhone())
.storeImage(storeEntity.getStoreImage())
// .productId(storeEntity.getProductIds())
.build();
readResponseDTOS.add(readResponseDTO);
}
responseDTO = ResponseDTO.builder()
.result((String) resultMap.get("result"))
.data(readResponseDTOS)
.build();
} else {
responseDTO = ResponseDTO.builder()
.result((String) resultMap.get("result"))
.data(null)
.build();
}
return responseDTO;
}
@Override
public SimpleResponseDTO toSimpleResponseDTO(Map<String, Object> resultMap) {
SimpleResponseDTO simpleResponseDTO;
simpleResponseDTO = SimpleResponseDTO.builder()
.result((String) resultMap.get("result"))
.build();
return simpleResponseDTO;
}
public String findImageKey(String url) {
// 정규 표현식 패턴을 정의
Pattern pattern = Pattern.compile("/image/download/(\\d+)");
// 패턴과 URL을 매칭
Matcher matcher = pattern.matcher(url);
String imageKey;
// 매칭된 부분 찾기
if (matcher.find()) {
// 숫자를 문자열로 추출
imageKey = matcher.group(1);
} else {
imageKey = null;
}
logger.info("ImageKey: " + imageKey);
return imageKey;
}
}
MSA에서는 여러 서비스 간의 호출로 구성이 된다. (서비스가 다른 서비스를 찾아서(즉, 디스커버리) 통신할 수 있도록 구성된다.)
각 서비스가 다른 서비스를 찾아서 통신하기 위해선 동적인 방법이 필요하다. 이를 위해 사용되는 것이 바로 서비스 디스커버리이다.
서비스 디스커버리는 크게 두 가지 방식으로 구현된다.
- 클라이언트 사이드 디스커버리: 클라이언트가 서비스 디스커버리를 직접 수행하는 방식이다. 클라이언트는 서비스 디스커버리 서버(Eureka, Consul 등)에 직접 요청하여 필요한 서비스의 위치 정보를 얻고, 이를 이용하여 서비스에 접속한다.
- 서버 사이드 디스커버리: 클라이언트가 API 게이트웨이 같은 중간 서버에 요청을 보내면, 중간 서버가 서비스 디스커버리를 수행하는 방식이다. 중간 서버는 서비스 디스커버리 서버에 요청하여 필요한 서비스의 위치 정보를 얻고, 클라이언트 대신 서비스에 접속한다.
해당 프로젝트에서 MSA를 채택해, 서비스간 통신을 통해 구현되었다.
Image와 Product는 서버를 따로 만들어 구현되었는데, 해당 서버들과 통신을 해야한다.
우리는
Eureka를 사용해서 구현했다.Eureka는 Netflix에서 개발하고 오픈 소스로 공개한 서비스 디스커버리 툴이다. 마이크로서비스 아키텍처에서 서비스 디스커버리는 여러 개의 독립적 서비스가 서로를 찾아서(즉, 디스커버리) 통신할 수 있게 해주는 중요한 역할을 한다.
Eureka는 주로 다음 두 가지 주요 컴포넌트로 이루어져 있다:
- Eureka Server: 서비스 인스턴스 정보를 유지하고 제공하는 중앙집중식 서버다. 서비스 인스턴스가 시작할 때 Eureka 서버에 자신의 정보를 등록하고, 주기적으로 상태를 업데이트(핫비트)한다. 만약 서비스 인스턴스가 정상적인 핫비트를 보내지 않으면, Eureka 서버는 해당 서비스 인스턴스를 등록 해제한다.
- Eureka Client: 서비스 인스턴스에 내장된 클라이언트로, Eureka 서버에 서비스 인스턴스를 등록하고 상태를 업데이트하는 역할을 한다. 또한, Eureka 클라이언트는 서비스 디스커버리를 위해 Eureka 서버로부터 서비스 인스턴스 정보를 조회할 수 있다.
Eureka서버에 등록하고, Eureka서버에 등록된 인스턴스(서버)와 RestTemplate로 Http요청을 보낼 때 필요한 정보들을 받을 수 있다.
각각의 마이크로서비스가 RestTemplate로 Http요청을 보낼 때 필요한 정보들을 받을 수 있다.
보통
Eureka서버에는API GATEWAY와 그 밖의 서버들을 두어 구현한다.
Eureka를 통해서Gateway에 요청을 보내고,Gateway가 적절한 서버로 라우팅을 해주는 역할을 한다.
서버간 통신을 통해서 구현되는 서비스는 따로 구현을 진행했다.
package com.wara.store.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.net.URISyntaxException;
public interface HttpCommunicationService {
// public Boolean productDelete(Long storeId) throws URISyntaxException;
public String imageUpload(MultipartFile image) throws URISyntaxException, IOException;
public String imageUpdate(MultipartFile image, String imageKey) throws URISyntaxException, IOException;
public Boolean imageDelete(String imageKey) throws URISyntaxException;
}
package com.wara.store.Service;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
//springframework.cloud는 분산 시스템을 구축하고 운영하기 위한 여러 기능과 도구를 제공하는 Spring 프레임워크의 확장판이다.
//서비스 인스턴스를 나타내는 클래스로, 각각의 서비스 인스턴스에 대한 정보를 담고 있다. (URI, port, host, serviceId 등)
//서비스 디스커버리 클라이언트의 인터페이스로, 여러 서비스 인스턴스를 관리하고 찾을 수 있는 메서드를 제공한다.
//Spring Cloud Netflix의 Eureka, Spring Cloud Consul 등에서 제공된다. Discovery Client를 사용하면 서비스 간 통신을 위해 동적으로 서비스 인스턴스를 찾을 수 있다.
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.LinkedHashMap;
import java.util.List;
@Service
public class HttpCommunicationServiceImpl implements HttpCommunicationService{
private final DiscoveryClient discoveryClient;
private final static Logger logger = LoggerFactory.getLogger(HttpCommunicationServiceImpl.class);
public HttpCommunicationServiceImpl(DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
}
// @Override
// public Boolean productDelete(Long storeId) throws URISyntaxException {
// try {
// ServiceInstance productService = discoveryClient.getInstances("PRODUCT-SERVICE").get(0);
// RestTemplate restTemplate = new RestTemplate();
// HttpHeaders headers = new HttpHeaders();
// HttpEntity<?> http = new HttpEntity<>(headers);
//
// URI uri = new URI(productService.getUri() + "/api/product/seller/store/" + storeId);
//
// ResponseEntity response = restTemplate.exchange(uri, HttpMethod.DELETE, http, String.class);
//
//
// if (response.getStatusCode().is2xxSuccessful()) {
// return true;
// }
//
// } catch (HttpClientErrorException e) {
// return false;
// }
//
// return false;
// }
@Override
public String imageUpload(@NotNull MultipartFile image) throws URISyntaxException, IOException {
ByteArrayResource body = new ByteArrayResource(image.getBytes()) {
@Override
public String getFilename() {
return image.getOriginalFilename();
}
};
try {
ServiceInstance imageService = discoveryClient.getInstances("IMAGE-SERVICE").get(0);
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
HttpEntity<?> http = new HttpEntity<>(headers);
MultiValueMap<String, Object> bodyMap = new LinkedMultiValueMap<>();
bodyMap.add("images", body);
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
http = new HttpEntity<>(bodyMap, headers);
URI uri = new URI(imageService.getUri() + "/api/image/upload");
ResponseEntity response = restTemplate.exchange(uri, HttpMethod.POST, http, LinkedHashMap.class);
if (response.getStatusCode().is2xxSuccessful()) {
LinkedHashMap responseBody = (LinkedHashMap) response.getBody();
List<String> images = (List) responseBody.get("images");
String imageUri = (String) images.get(0);
return imageUri;
}
} catch (HttpClientErrorException e) {
return "Failed to upload image";
}
return "Failed to upload image";
}
@Override
public String imageUpdate(@NotNull MultipartFile image, String imageKey) throws URISyntaxException, IOException {
ByteArrayResource body = new ByteArrayResource(image.getBytes()) {
@Override
public String getFilename() {
return image.getOriginalFilename();
}
};
try {
ServiceInstance imageService = discoveryClient.getInstances("IMAGE-SERVICE").get(0);
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
MultiValueMap<String, Object> bodyMap = new LinkedMultiValueMap<>();
ResponseEntity response;
if (imageKey != null) {
bodyMap.add("image", body);
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<?> http = new HttpEntity<>(bodyMap, headers);
URI uri = new URI(imageService.getUri() + "/api/image/" + imageKey);
response = restTemplate.exchange(uri, HttpMethod.PUT, http, String.class);
logger.info("ImageServer PUT Method");
if (response.getStatusCode().is2xxSuccessful()) {
String imageUri = (String) response.getBody();
return imageUri;
}
} else {
bodyMap.add("images", body);
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<?> http = new HttpEntity<>(bodyMap, headers);
URI uri = new URI(imageService.getUri() + "/api/image/upload");
response = restTemplate.exchange(uri, HttpMethod.POST, http, LinkedHashMap.class);
logger.info("ImageServer POST method");
if (response.getStatusCode().is2xxSuccessful()) {
LinkedHashMap responseBody = (LinkedHashMap) response.getBody();
List<String> images = (List) responseBody.get("images");
String imageUri = (String) images.get(0);
return imageUri;
}
}
} catch (HttpClientErrorException e) {
return "Failed to upload image";
}
return "Failed to upload image";
}
@Override
public Boolean imageDelete(String imageKey) throws URISyntaxException{
try {
ServiceInstance imageService = discoveryClient.getInstances("IMAGE-SERVICE").get(0);
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
HttpEntity<?> http = new HttpEntity<>(headers);
ResponseEntity response;
if (imageKey != null) {
URI uri = new URI(imageService.getUri() + "/api/image/" + imageKey);
response = restTemplate.exchange(uri, HttpMethod.DELETE, http, Boolean.class);
logger.info("ImageServer DELETE Method");
return (Boolean) response.getBody();
} else {
return true;
}
} catch (HttpClientErrorException e) {
return false;
}
}
}
: 클라이언트의 요청을 처리하는 Controller 클래스를 생성합니다. 이 클래스에서는 HTTP 요청을 받아 적절한 Service 메서드를 호출하고, 그 결과를 HTTP 응답으로 반환합니다.
@RestController는 @Controller에 @ResponseBody가 추가된 것이다.
package com.wara.store.Controller;
import com.wara.store.Service.StoreService;
import com.wara.store.DTO.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/api/store")
public class StoreController {
private final StoreService storeService;
private final static Logger logger = LoggerFactory.getLogger(StoreController.class);
public StoreController(@Autowired StoreService storeService) {
this.storeService = storeService;
}
@GetMapping(value = "/exist/id/{storeId}")
public ResponseEntity<SimpleResponseDTO> existStoreById(@PathVariable("storeId") Long storeId) {
logger.info("[Request] : ExistStoreById");
logger.info("[Data]: " + storeId);
SimpleResponseDTO response = storeService.existStoreById(storeId);
logger.info("[Response] : " + response.toString());
if(response.getResult().equals("success")){
return ResponseEntity.status(200).body(response);
}
return ResponseEntity.status(400).body(null);
}
@GetMapping(value = "/exist/seller/{storeSeller}")
public ResponseEntity<SimpleResponseDTO> existStoreBySeller(@PathVariable("storeSeller") String storeSeller) {
logger.info("[Request] : ExistStoreBySeller");
logger.info("[Data]: " + storeSeller);
SimpleResponseDTO response = storeService.existStoreBySeller(storeSeller);
logger.info("[Response] : " + response.toString());
if(response.getResult().equals("success")){
return ResponseEntity.status(200).body(response);
}
return ResponseEntity.status(400).body(null);
}
@PostMapping(value = "/create", consumes = "application/json")
public ResponseEntity<SimpleResponseDTO> createStore(@RequestBody StoreDTO storeDTO) {
logger.info("[Request] : CreateStore(No Image)");
logger.info("[Data]: " + storeDTO.toString());
SimpleResponseDTO response = storeService.createStore(storeDTO);
logger.info("[Response] : " + response.toString());
if(response.getResult().equals("success")){
return ResponseEntity.status(201).body(response);
}
return ResponseEntity.status(400).body(null);
}
@PostMapping(value = "/create", consumes = "multipart/form-data")
public ResponseEntity<SimpleResponseDTO> createStore(@RequestPart("image") MultipartFile image,
@RequestPart("json") StoreDTO storeDTO) {
logger.info("[Request] : CreateStore(Image)");
logger.info("[Data]: " + storeDTO.toString());
SimpleResponseDTO response = storeService.createStore(storeDTO, image);
logger.info("[Response] : " + response.toString());
if(response.getResult().equals("success")){
return ResponseEntity.status(201).body(response);
}
return ResponseEntity.status(400).body(null);
}
@GetMapping(value = "/read/id/{storeId}")
public ResponseEntity<ResponseDTO> readStoreById(@PathVariable("storeId") Long storeId) {
logger.info("[Request] : ReadStoreById");
logger.info("[Data]: " + storeId);
ResponseDTO response = storeService.readStoreById(storeId);
logger.info("[Response] : " + response.toString());
if(response.getResult().equals("success")){
return ResponseEntity.status(200).body(response);
}
return ResponseEntity.status(400).body(null);
}
@GetMapping(value = "/read/seller/{storeSeller}")
public ResponseEntity<ResponseDTO> readStoreBySeller(@PathVariable("storeSeller") String storeSeller) {
logger.info("[Request] : ReadStoreBySeller");
logger.info("[Data]: " + storeSeller);
ResponseDTO response = storeService.readStoreBySeller(storeSeller);
logger.info("[Response] : " + response.toString());
if(response.getResult().equals("success")){
return ResponseEntity.status(200).body(response);
}
return ResponseEntity.status(400).body(null);
}
@PutMapping(value = "/update/id", consumes = "application/json")
public ResponseEntity<SimpleResponseDTO> updateStoreById(@RequestBody StoreDTO storeDTO) {
logger.info("[Request] : UpdateStoreById(No Image)");
logger.info("[Data]: " + storeDTO.toString());
SimpleResponseDTO response = storeService.updateStoreById(storeDTO);
logger.info("[Response] : " + response.toString());
if(response.getResult().equals("success")){
return ResponseEntity.status(200).body(response);
}
return ResponseEntity.status(400).body(null);
}
@PutMapping(value = "/update/id", consumes = "multipart/form-data")
public ResponseEntity<SimpleResponseDTO> updateStoreById(@RequestPart("image") MultipartFile image,
@RequestPart("json") StoreDTO storeDTO) {
logger.info("[Request] : UpdateStoreById(Image)");
logger.info("[Data]: " + storeDTO.toString());
SimpleResponseDTO response = storeService.updateStoreById(storeDTO, image);
logger.info("[Response] : " + response.toString());
if(response.getResult().equals("success")){
return ResponseEntity.status(200).body(response);
}
return ResponseEntity.status(400).body(null);
}
@DeleteMapping(value = "/delete/id/{storeId}")
public ResponseEntity<SimpleResponseDTO> deleteStore(@PathVariable("storeId") Long storeId) {
logger.info("[Request] : DeleteStoreById");
logger.info("[Data]: " + storeId);
SimpleResponseDTO response = storeService.deleteStore(storeId);
logger.info("[Response] : " + response.toString());
if(response.getResult().equals("success")){
return ResponseEntity.status(200).body(response);
}
return ResponseEntity.status(400).body(null);
}
// @DeleteMapping(value = "/delete/{storeId}/{productId}")
// public ResponseEntity<SimpleResponseDTO> deleteProduct(@PathVariable Long storeId,
// @PathVariable Long productId){
// logger.info("[Request] : DeleteProductId");
// logger.info("[Data]: " + storeId + " " + productId);
// SimpleResponseDTO response = storeService.deleteProduct(storeId, productId);
// logger.info("[Response] : " + response.toString());
//
// if(response.getResult().equals("success")){
// return ResponseEntity.status(200).body(response);
// }
// return ResponseEntity.status(400).body(null);
// }
}
: 각 클래스의 기능을 검증하기 위해 테스트 코드를 작성합니다. Spring Boot는 @SpringBootTest 애노테이션을 이용한 통합 테스트를 쉽게 작성할 수 있게 지원합니다.
: 모든 설정이 완료되면, 애플리케이션을 실행합니다. IDE에서 제공하는 실행 기능을 사용하거나, 콘솔에서 mvn spring-boot:run 명령을 사용할 수 있습니다.
이러한 과정을 통해 Spring Boot를 이용해 서버를 만들 수 있습니다. 각 단계에 따라 필요한 코드와 설정은 프로젝트의 요구사항에 따라 달라질 수 있습니다.
배포관련 내용은 양이 많아 따로 작성해두었으니 참고하도록.
지후의 배포관련 게시글