이 글은 GDSC Ewha 서버 파트의 2024 솔루션 챌린지를 위해 진행한 Springboot 미니 프로젝트를 설명하는 글입니다. GDSC Ewha의 활동을 더 보고 싶다면 여기를 참고해주세요.
프로젝트의 깃허브 레포지토리 링크
프로젝트를 GCP Cloud Storage에 연결
GCP Cloud Storage 생성
이제 GCP Cloud Storage를 생성해 보겠습니다. 먼저 GCP Cloud Storage로 이동해 버킷 만들기를 클릭합니다.
버킷 이름은 원하는 대로 지정합니다. 참고로 버킷은 글로벌한 리소스로, 다른 사람이 이미 만든 버킷 이름으로는 만들 수 없습니다.
데이터 저장 위치는 리전으로 선택하고, asia-northeast3(서울)로 지정합니다.
스토리지 클래스는 “기본 클래스 설정”의 Standard를 선택합니다.
그 후, 객체 액세스를 제어하는 방식을 “이 버킷에 공개 액세스 방지 적용”을 선택하고, 액세스 제어를 “세분화된 제어”로 선택합니다.
설정을 이렇게 세팅한 후, “만들기”를 클릭해서 버킷을 생성해줍니다.
GCP Cloud Storage에 접근권한 가지기
지금 이 버킷은 “이 버킷에 공개 액세스 방지 적용”이 적용되어 있습니다. 따라서 버킷에 접근하기 위해서는 접근 권한이 필요합니다. 먼저 “IAM 및 관리자”에서, 서비스 계정 탭으로 들어가 새로운 서비스 계정을 생성해줍니다.
서비스 계정의 이름을 지어주고, 간단한 설명을 적습니다.
그 후 서비스 계정의 권한을 설정해줍니다. 권한은 객체 나열, 생성, 보기, 삭제 등 객체에 대한 전체 제어 권한을 부여하는 “저장소 개체 관리자”와, 버킷과 객체에 대한 전체 제어 권한을 부여하는 “저장소 관리자” 두 개를 선택합니다.
그 후 생성한 서비스 계정에서, “키 관리”를 선택합니다.
새 키 만들기를 클릭합니다.
json 키를 생성합니다. 생성하면 자동으로 로컬에 다운로드 받아집니다. 저는 solution-challenge-412104-328059455d14.json라는 이름으로 다운로드 받아졌습니다. 키는 절대로 유출되지 않도록 합니다.
Spring boot와 Cloud Storage 연동
이제 아까 발급 받은 json 키를 spring boot 프로젝트에 적용시켜 접근 권한을 가지도록 합니다. 먼저, build.gradle에 다음과 같이 종속성을 추가합니다. 종속성을 추가한 후에는 Gradle refresh를 통해서 종속성을 다운로드 받아줍니다. (아니면 처음부터 start.spring.io 에서 Google Cloud Storage 종속성을 추가합니다.)
ext {
set('springCloudGcpVersion', "5.0.0")
set('springCloudVersion', "2023.0.0")
}
dependencies {
//GCP
implementation 'com.google.cloud:spring-cloud-gcp-starter-storage'
}
dependencyManagement {
imports {
mavenBom "com.google.cloud:spring-cloud-gcp-dependencies:${springCloudGcpVersion}"
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
그 후 위에서 만든 json 키를 프로젝트에 포함시키려고 합니다. 그러나 이 파일은 절대로 깃허브에 올라가지 않게 만들어야 합니다. 따라서 .gitignore 파일에 포함시켜, 깃허브에 올라가지 않도록 만듭니다.
.gitignore 파일 맨 하단에 다음과 같이 추가합니다.
### GCP Key File ###
solution-challenge-412104-328059455d14.json
그 후, resources 폴더에 해당 solution-challenge-412104-328059455d14.json 파일을 그대로 넣어줍니다.
정상적으로 .gitignore 설정이 적용되었다면, 이런 식으로 GitHub Desktop 등 GUI에서도 변경 사항 목록에 solution-challenge-412104-328059455d14.json 파일이 존재하지 않습니다. 이제 키 파일은 로컬에만 존재하고, 원격(깃허브)에는 업로드되지 않습니다. 이런식으로 모든 민감한 설정들을 깃허브에 업로드되지 않도록 만들어줄 수 있습니다. 참고로, 저는 application.yml 파일을 참고용으로 깃허브에 올리지만, application.yml 파일도 여러 민감한 정보를 가지고 있는 만큼 .gitignore에 추가하여 깃허브에 올라가지 않도록 해주는 게 좋습니다.
이제 키 파일이 깃허브에 올라가지 않도록 바꾸어주었으니, 이 키를 적용해야 합니다. spring-cloud-gcp-starter-storage 종속성은 환경변수 GOOGLE_APPLICATION_CREDENTIALS 값에서 이 JSON 키 파일의 경로를 받아와 적용하는 역할을 합니다.
환경변수는 윈도우/맥에서 직접 설정해줄 수도 있지만, 지금은 인텔리제이에서 설정해주도록 하겠습니다. 우측 상단의 프로젝트를 선택한 후, 설정에서 Edit을 선택합니다.
그 후 Modifiy Options를 선택한 후, Environment variables를 선택합니다.
그 후 GOOGLE_APPLICATION_CREDENTIALS 변수에 JSON 키 파일 절대 경로를 등록해주면 됩니다.
JSON 키 파일 절대 경로는 해당 파일을 우클릭한 후, Copy Path로 얻을 수 있습니다.
# 다음과 같이 등록
GOOGLE_APPLICATION_CREDENTIALS=키파일/경로를/명시합니다
그 후, application.yml로 이동해서 다음과 같이 버킷 이름과 프로젝트 이름을 적어줍니다.
# GCP Cloud Storage
storage:
bucket: [버킷 이름]
project-id: [프로젝트 이름]
이제 GCP Cloud Storage에 대한 세팅이 완료되었습니다.
Dto 만들기
클라이언트에게 데이터를 넘겨 줄 때, 엔티티 그 자체로 넘겨줘도 됩니다. 다음과 같이 ResponseEntity 안에, DB에서 검색한 엔티티 그 자체를 바로 넘겨줄 수 있습니다. 그러면 이 엔티티의 필드들은 자동으로 JSON 객체로 변환됩니다.
@GetMapping("/API 엔드포인트 1")
public ResponseEntity<SampleEntity> getData(@RequestBody Long id){
SampleEntity1 sampleEntity1 = sampleRepository.findById(id);
return new ResponseEntity<>(sampleEntity1, HttpStatus.OK);
}
예를 들어, 다음과 같은 엔티티라면:
@Entity
@Getter
public class SampleEntity1 {
@Id
@GeneratedValue
@Column(name="circle_id")
private Long id;
private String data;
@OneToMany(mappedBy = "sampleEnitity1")
private List<SampleEntity2> sampleEntity2= new ArrayList<>();
}
컨트롤러에서 다음과 같은 JSON 값으로 자동으로 변환되어 반환됩니다:
{
"id": 1,
"data": "some data",
"sampleEntity2": [
{
"id": 101,
"data": "data for SampleEntity2-1"
},
{
"id": 102,
"data": "data for SampleEntity2-2"
},
// ... (다른 SampleEntity2 항목들)
]
}
하지만 우리는 엔티티 그 자체를 바로 return하기 보다는, DTO(Data Transfer Object, 데이터 전송 객체)라는 객체를 만들어서 이 객체 안에 우리가 넘겨주고 싶은 값들을 담아서, 이 객체를 넘겨주려고 합니다. 말하자면 다음과 같은 구조라고 할 수 있습니다.
그렇다면 이 DTO는 왜 사용하는 걸까요?
이유 1: JPA 무한 순환 문제 방지
그 이유는 다음과 같습니다.
위의 예시에서 SampleEntity1를 반환 할 때, 해당 엔티티가 참조하고 있는 SampleEntity2까지도 반환된 것을 볼 수 있습니다. 그 이유는 단순한 SQL은 외래키 값 하나로 두 개의 테이블을 join할 수 있지만, 자바에서는 어떤 엔티티 클래스의 키 값을 조회하기 위해서는 객체 자체를 필드로 포함하기 때문입니다.
이러한 특징 때문에 JPA 무한 순환 문제가 발생할 수 있습니다. 예를 들어, 다음과 같이 서로 양방향 참조를 하는 엔티티 3개를 생각해보겠습니다.
그러면 엔티티 하나만 조회해도, 다음과 같은 무한 순환하는 JSON 값이 반환될 수 있습니다.
{
"id": 1,
"data": "data for Entity1",
"entity2List": [
{
"id": 101,
"data": "data for Entity2",
"entity1": {
"id": 1,
"data": "data for Entity1",
"entity2List": [
// 무한히 반복
]
},
"entity3List": [
{
"id": 1001,
"data": "data for Entity3",
"entity2": {
"id": 101,
"data": "data for Entity2",
"entity1": {
"id": 1,
"data": "data for Entity1",
"entity2List": [
// 무한히 반복
]
},
"entity3List": [
// 무한히 반복
]
}
},
// 무한히 반복
]
},
// 무한히 반복
]
}
이런 문제를 방지하기 위해 DTO를 사용합니다.
@JsonIgnore 또는 fetch = FetchType.LAZY 사용하기
참고로, 이러한 무한 순환 문제는 DTO가 아니더라도 다음과 같은 방식으로 없앨 수 있습니다.
@OneToMany(mappedBy = "entity2", fetch = FetchType.LAZY)
@JsonIgnore
private List<Entity3> entity3List = new ArrayList<>();
- fetch = FetchType.LAZY: 특정 엔티티를 조회할 때 연관된 엔티티들을 즉시 가져오지 않고, 해당 연관 엔티티의 실제로 접근(로딩)이 필요한 시점에 가져오는 옵션입니다. 예를 들어, Entity1을 조회할 때는 Entity2를 즉시 가져오지 않습니다. 대신, Entity2에 실제로 접근(로딩)이 필요한 시점, 예를 들어 Entity1.getEntity2List()를 호출할 때 해당 Entity2들을 가져오게 됩니다.
- @JsonIgnore: JSON 직렬화 시에 해당 필드를 무시합니다. 즉 엔티티가 JSON으로 변경될 때, 특정 방향의 참조를 무시하도록 설정합니다.
두 어노테이션 중 상황에 맞게 골라서(혹은 둘 다) 사용하면 됩니다. 그러나 이러한 옵션들은 필드에 종속적이므로, 옵션을 사용할 때와 사용하지 않을 때를 선택할 수 없습니다. 예를 들어 @JsonIgnore 옵션을 사용하면, 참조를 사용하고 싶을 때도 사용할 수 없습니다. 따라서 DTO/@JsonIgnore/fetch = FetchType.LAZY는 각각 상황에 맞게 사용하면 됩니다.
이유 2: JSON과 객체 간 자동 맵핑되게 하기
클라이언트에서 넘겨주는 JSON 값을 받을 때, Dto 객체가 있으면 자동으로 JSON의 키-값 쌍이 Dto 객체의 필드명-값으로 맵핑됩니다. 따라서 Request는 반드시 Dto를 만들어주는 게 좋습니다.
이유 3: API 스펙 안 변경되도록 하기
프로젝트를 진행하다보면 DB의 구조를 변경해야 하는 경우도 종종 있습니다. 연관관계가 달라지는 큰 변경부터, 필드 하나 추가 등 간단한 변경이 있을 수도 있습니다. 이럴 때 엔티티 그 자체를 바로 return하는 방식을 쓰면, DB 구조 변경=API 스펙이 변경이 됩니다. 이 경우에는 해당 API 스펙에 맞추어 개발하고 있던 프론트엔드 개발자도 코드를 수정해야 하는 일이 생길 때가 있습니다.
그러나 DTO를 사용하면, 백엔드 내부 로직이 어떻게 변경되던 간에 API 스펙을 최대한 변경 없이 개발을 진행할 수 있습니다.
이유 4: Entity 스펙 숨기기
Entity의 스펙(필드, 연관관계 등)은 DB의 내부 구조를 그대로 보여주는 값들입니다. 만약 엔티티를 바로 클라이언트에게 반환하면 이 내부 구조를 노출시키는 셈이 됩니다. 또한, 엔티티의 필드 중에서는 클라이언트에게 전달해서는 안되는 민감한 정보가 있을 수도 있습니다. DTO를 사용하면 정확하게 클라이언트가 사용해도 되는 필드 값만 넘겨줄 수 있기 때문에 보안에도 좋습니다.
다음 주차
이번 주차에서는 GCP Cloud Storage를 연결하고, Dto를 만드는 법과 만드는 이유에 대해서 알아보았습니다. 다음 주차에서는 이제 실제로 API를 개발하겠습니다.
'🗄️Backend > [GDSC] SpringBoot 프로젝트' 카테고리의 다른 글
Spring Boot 단기 프로젝트: 6. Make API(Auth, Circle) (0) | 2024.02.08 |
---|---|
Spring Boot 단기 프로젝트: 5. Make API(Profile, Posts) (0) | 2024.02.08 |
Spring Boot 단기 프로젝트: 3. RestController & Swagger-UI & Postman (0) | 2024.02.08 |
Spring Boot 단기 프로젝트: 2. Build Entity & Connect to GCP Cloud SQL (0) | 2024.02.08 |
Spring Boot 단기 프로젝트: 1. Build RDB & Class Diagram (1) | 2024.02.08 |