서론
프로젝트에서 DynamoDB에 연결하여 데이터 저장할 일이 있었다. DynamoDB CRUD의 Example을 검색했으나, DynamoDBMapper를 사용하여 데이터를 기록하거나, Spring Data dynamoDB를 사용하더라도 local DynamoDB에 연결하여 local에 기록하는 예시들이 대부분이었다. 즉, AWS 서버 DynamoDB에 Spring-data-dynamoDB를 통해 데이터를 기록하는 예시는 찾아보기 어려웠다.
글쓴이는 장시간의 검색과 삽질 끝에 AWS 서버의 DynamoDB에 연결하여 Spring-data-dynamoDB에 데이터 쓰기를 성공했다. 막상 성공하고 보니 코드도 간단하고 정말 별거 아닌데, 작성되어있는 예시가 없다보니 한참을 헤맸던 것이다. 본 글은 이 글을 보는 다른 사람들은 그렇게 헤매지 않기를 바라는 마음에 작성한 것이다.
요약해서 말하자면, Spring-data-dynamoDB를 사용하여 AWS 서버 DynamoDB에 CRUD를 하고자 하는 사람은 이 글을 참고하면 되겠다.
DynamoDBMapper vs Spring-Data-DynamoDB
본론으로 들어가기 앞서서 잠깐 DynamoDBMapper와 Spring-Data-DynamoDB의 차이를 간략하게 비교해보겠다. DynamoDBMapper는 AWS에서 공식적으로 제공해주는 AWS JAVA SDK에 들어있는 클래스의 일종이다. 아래는 AWS 공문서에 나온 DynamoDBMapper의 정보다.
반대로 Spring-data-DynamoDB는 AWS에서 제공하는 서비스가 아니다. 그럼 Spring에서 제공하는 기술인가? 그것도 아니다. Spring은 Spring-data-JPA, Spring-data-mongoDB, Spring-data-cassandra 등 여러가지 Spring data 인터페이스를 제공하지만 이 DynamoDB에 대해서는 제공하지 않고 있다. 그럼 현재 검색을 통해 확인할 수 있는 Spring-data-dynamoDB는 무엇일까? 그냥 개인이 만들어놓은 프로젝트를 dependency로 가져오는 것이다. Spring에서 공식적으로 제공하는 서비스는 아니다. 하지만 여타 spring data 인터페이스와 동작법은 거의 같다. 그러므로 Spring data JPA를 사용해봤던 사람이라면 금방 적응하여 사용할 수 있을 것이다.
가장 범용적으로 많이 사용되는 Spring data dynamoDB는 아래 derjust저장소의 dependency다. 글쓴이도 해당 github를 참고하여 spring data dynamoDB를 성공적으로 동작시킬 수 있었다.
https://github.com/derjust/spring-data-dynamodb
DynamoDBMapper
DynamoDBMapper는 AWS에서 제공하는 AWS JAVA SDK에 들어있는 클래스의 일종으로, 객체와 테이블을 매핑해주는 함수 역할을 한다. 이 Mapper를 사용하여 JAVA 메서드를 사용하면 메서드가 Key-Value DB인 DynamoDB의 쿼리로 매핑이 되어 간편하게 CRUD를 수행할 수 있다. 일례로 아래와 같이 사용한다. 직접 따라 구현하지는 말고, 느낌만 살펴보도록 하자.
@Configuration에서 DynamoDBMapper 선언
DynamoDBMapper를 사용하려면 먼저 DynamoDBMapper 인스턴스를 생성해야한다. 본 예시에서는 DynamoDBMapper를 생성하기 위해서 buildAmazonDynamoDB() 함수를 사용했다.
@repository에서 DynamoDBMapper 메서드 사용
DynamoDBMapper를 사용하여 CRUD 쿼리 메서드를 매핑한 것이다. dynamoDBMapper.save()를 사용하면 DynamoDB에서 Create나 Update를 할 수 있다. dynamoDBMapper.load()를 사용하면 DB에서 Read를 사용할 수 있다. delete 메서드도 마찬가지로 다이나모 테이블 쿼리에 매핑된다. 다만 update의 경우에는 보이는 바와 같이 사용 방법이 조금 까다롭다. Create의 경우와 똑같이 save() 메서드를 사용하지만 추가적으로 필요한 메서드들이 많이 있다.
DynamoDBMapper를 사용하기 위해서는 Spring boot 내에서 AWS와 연결하기 위한 셋업도 필요하다. 본 글에서는 DynamoDBMapper를 사용한 CRUD는 구현하지 않을 것이다. 위 코드는 그냥 DynamoDBMapper의 느낌이 어떤지 보는 용도로 살펴보도록 하자.
Spring-data-dynamoDB
DynamoDB에 쿼리를 날릴 선택지로 DynamoDB Mapper도 나쁘지 않은 선택지다. 하지만 위에 보이듯이 Update 쿼리나 조건을 덧붙이는 등 좀 더 복잡한 동작을 하려면 코드가 살짝 복잡해지는 경향이 있다.
Spring-data-dynamoDB는 Spring의 CrudRepository interface를 이용하여 복잡한 쿼리도 하나의 문장으로 표현하여 손쉽게 동작시킬 수 있다. 아래는 spring data 쿼리 메소드 코드 예시다.
이처럼 spring data 에서 제공하는 쿼리 메소드 양식대로 메소드 이름을 작성한다면 spring에서 CrudRepository 인터페이스를 활용하여 알아서 메소드를 쿼리로 해석해준다.
활용 가능한 쿼리 메소드는 아래 Spring 공식 웹사이트에서 확인할 수 있다. Spring data dynamoDB도 Spring data JPA와 똑같은 형식의 쿼리 메소드를 사용하므로 아래 글을 참고하여 쿼리 메소드를 사용하면 되겠다.
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods
Spring data dynamoDB 사용하기
Spring 프로젝트 셋업
<프로젝트 환경>
JAVA: 19
Spring boot: 3.0.0
Build tools: Gradle
dependency: spring-data-dynamodb
dynamodb는 spring측에서 공식 제공하는 라이브러리가 없다. 그래서 개인이 프로젝트로 생성한 spring-data 라이브러리를 사용해야한다. AWS에 연결하는 Configuration 설정하고 spring data dynamodb의 쿼리를 사용하기 위해서 아래 dependency를 가져온다.
implementation 'com.github.derjust:spring-data-dynamodb:5.1.0'
AWS console에서 access key 생성
DynamoDB에 데이터를 넣기 앞서서 AWS에서 제공하는 리소스를 로컬 코드에서 사용하기 위해서는 AWS IAM에서 제공하는 Access key와 Secret key가 필요하다.
AWS IAM에 들어가서 사용자 탭에 들어간 후 현재 프로젝트에 사용하는 DynamoDB 사용자를 선택한다. 만약 생성해놓은 AWS 사용자가 없다면 새로 생성해주고 DynamoDB에 접근할 수 있는 권한 정책을 가지도록 설정해주자. 어떤 권한을 넣어줘야할지 잘 모르겠다면 아래와 같이 우선 DynamoDBFullAccess를 넣어주면 된다. 본 권한은 DynamoDB 리소스의 모든 동작을 사용할 수 있는 권한을 의미한다. 또한 Accesskey를 생성할 수 있는 권한도 해당 사용자에게 붙여줘야한다.
그런 다음, 권한 탭 옆에 있는 보안 자격 증명 탭으로 들어가서 아래로 쭉 내리면 액세스키라는 항목이 있다. 액세스 키 만들기를 눌러서 아래와 같이 로컬 코드 용도로 생성해주도록 한다.
액세스 키 생성이 완료되면 1회 한정으로 액세스키와 비밀키 내용을 확인할 수 있다. 이후 데이터를 사용하기 위해서 CSV 파일로 다운로드 받아놓는다.
DynamoDB 사용을 위한 Spring Configuration 설정
액세스키를 생성해뒀으므로 이제 이 액세스키와 비밀키를 활용하여 로컬 코드에서 AWS의 DynamoDB 서비스로 접근할 수 있도록 Configuration 로컬 코드를 작성할 차례다.
DynamoDBConfiguration
AWS DynamoDB에 연결하여 사용하기 위해서는 어플리케이션 코드 단에서 아래와 같이 설정하는 코드가 필요하다.
@Configuration
@EnableDynamoDBRepositories(basePackages = "msa.customer.repository")
public class DynamoDBConfig {
@Value("${aws.accessKey}")
private String awsAccessKey;
@Value("${aws.secretKey}")
private String awsSecretKey;
@Value("${aws.region}")
private String awsRegion;
public AWSCredentials amazonAWSCredentials() {
return new BasicAWSCredentials(awsAccessKey, awsSecretKey);
}
public AWSCredentialsProvider amazonAWSCredentialsProvider() {
return new AWSStaticCredentialsProvider(amazonAWSCredentials());
}
@Bean
public AmazonDynamoDB amazonDynamoDB() {
return AmazonDynamoDBClientBuilder.standard().withCredentials(amazonAWSCredentialsProvider())
.withRegion(awsRegion).build();
}
}
Spring으로 DynamoDB를 사용하려면 기본적으로 AmazonDynamoDB를 Bean 객체로 등록해야 한다. 그 객체에는 사용자의 액세스키, 비밀키 그리고 사용 지역이 각각 등록되어있어야 한다. 마지막으로 .build 메소드를 사용하여 AmazonDynamodB를 생성하여 AWS에 연결한다.
@Value는 spring에서 제공하는 Annotation이다. application.properties에 있는 변수를 가져와 사용할 수 있게 해준다. 반드시 "${}" 내부에 해당 변수 이름을 작성해야한다.
application.properties
AWS 액세스키와 비밀키는 코드에 직접 작성하면 안 된다. 코드에 직접 작성하게 되면 github에 함께 업로드 되기 때문이다. AWS에 접근할 수 있는 비밀 액세스키가 모두에게 보여질수 있는 저장소에서 보여지길 원하지 않을 것이다. 그러므로 이런 비밀에 해당하는 정보는 .env 파일에 담아두고 코드에서 호출하여 사용하는 것이 좋다. application.properties는 .env에 작성된 변수를 호출하기 위한 중간 다리라고 생각하면 된다.
spring.config.import=optional:file:.env[.properties]
# AWS IAM
aws.accessKey=${AWS_ACCESS_KEY}
aws.secretKey=${AWS_SECRET_KEY}
위와 같이 spring.config.import를 작성하면 .env에 해당하는 파일에서 정보를 가져와 application.properties에 적용하고 application.properties에 등록된 변수는 코드에 반영되게 된다.
.env
env 파일에는 아래 보이는 등호의 오른쪽에 access key와 secret key를 각각 복사해서 붙여넣는다. 아까 다운로드 받은 CSV 파일에 해당 정보가 있을 것이다.
AWS_ACCESS_KEY=
AWS_SECRET_KEY=
DynamoDB에 넣을 데이터 양식 Entity 생성
아래와 같이 Entity(또는 DAO)를 생성해준다. 글쓴이는 Getter와 Setter 그리고 생성자는 lombok 애노테이션을 활용하여 생성했다.
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@DynamoDBTable(tableName = "delivery_customer")
public class Member {
@DynamoDBHashKey
private String memberId;
@DynamoDBAttribute
private String name;
@DynamoDBAttribute
private String email;
@DynamoDBAttribute
private String phoneNumber;
@DynamoDBAttribute
private String address;
}
@DynamoDBTable 애노테이션을 사용하면 테이블의 이름을 명명할 수 있다. @DynamoDBHashKey는 primary key를 나타내는 것이고 @DynamoDBAttribute는 나머지 column을 의미한다. @DynamoDBAutoGeneratedKey를 Primary key에 붙이면 해당 값은 DynamoDB에 의해 자동생성된다. 본 글쓴이는 cognito에서 생성된 id값을 가져올 것이므로 키 자동생성기능은 사용하지 않을 것이다.
AWS console에서 DynamoDB 생성
AWS console에 로그인하여 DynamoDB 서비스로 들어간다. 우측에 테이블 생성을 누른다.
테이블 이름에는 방금 전에 코드에 작성한 테이블 명을 가져와 넣고, 파티션 키에는 @DynamoDBHashKey로 지정한 변수의 이름을 가져온다.
테이블 생성을 누르면 해당 테이블이 생성되고 원격으로 사용할 수 있게된다.
본 작업은 AWS Console 뿐만 아니라 AWS CLI를 사용하여 수행할 수도 있다.
CrudRepository 인터페이스 상속
spring에서 제공하는 CrudRepository를 상속받아 인터페이스를 작성하면 메소드로 쿼리를 동작시킬수 있다. 지금은 특별히 등록할 메소드는 없다.
@EnableScan
public interface SpringDataDynamoMemberRepository extends CrudRepository<Member, String> {
}
repository 생성
본 글쓴이는 repository의 인터페이스를 먼저 만들고 repository의 class를 별도로 만들어 구현하는 것을 선호한다. 그래야 나중에 DB를 바꾸기 용이하기 때문이다. (클래스 간의 종속을 낮춤) 하지만 이는 선택사항으로 아래 인터페이스는 굳이 만들지 않고, 그냥 바로 클래스로 구현해도 된다.
repository interface
public interface MemberRepository {
public void make(Member member);
Optional<Member> findById(String id);
void setName(String id, String name);
void setPhoneNumber(String id, String phoneNumber);
void setAddress(String id, String address);
}
repository class
spring data의 CrudRepository 인터페이스를 사용하면 아래와 같이 repository.findById()는 기본적으로 제공되는 쿼리 메소드라서 따로 interface에 등록하지 않아도 정상적으로 동작하게 된다.
@Repository
@Primary
public class SpringDataMemberRepository implements MemberRepository{
private final SpringDataDynamoMemberRepository repository;
public SpringDataMemberRepository(SpringDataDynamoMemberRepository repository) {
this.repository = repository;
}
@Override
public void make(Member member) {
repository.save(member);
}
@Override
public Optional<Member> findById(String id) {
return repository.findById(id);
}
}
repository 메소드를 동작시키는 service class 생성
repository를 사용하기 위한 로직을 service 클래스에서 담당한다. 아래에는 이미 있는 사용자인지 체크하는 로직과 현재 DB에 없는 사용자는 회원가입을 시켜 DB에 저장하는 로직이 있다.
repository를 interface를 사용하여 구현체를 별도로 만들었다면 Bean 연결을 위해 @Autowired를 반드시 사용하도록 한다.
@Service
public class JoinService {
private final MemberRepository memberRepository;
@Autowired
public JoinService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
public Boolean checkJoinedMember(String cognitoUsername){
Optional<Member> user = memberRepository.findById(cognitoUsername);
return user.isPresent();
}
public void joinMember(String cognitoUsername, String email, String phoneNumber){
Member member = new Member();
member.setMemberId(cognitoUsername);
member.setEmail(email);
member.setPhoneNumber(phoneNumber);
memberRepository.make(member);
}
}
Member Controller 동작하여 회원가입, DB에 데이터 넣기
방금 생성한 joinService와 이전에 생성해둔 parseJwtService를 활용하여 회원가입을 하는 로직을 contoller에 구현한다. parseJwt는 cognito의 반환값으로 받은 jwt를 decode하여 정보를 끄집어내는 역할을 한다. jwt의 payload에는 cognito:username이라는 claim이 존재하는데, 이것을 유저 식별 id로 사용하는 것이다. 이 id를 DB의 Hashkey로 사용한다.
@RestController
public class HomeController {
private final ParseJwtService parseJwtService;
private final JoinService joinService;
public HomeController(ParseJwtService parseJwtService, JoinService joinService) {
this.parseJwtService = parseJwtService;
this.joinService = joinService;
}
@GetMapping("/customer/main-gate")
@ResponseStatus(HttpStatus.OK)
public void main(@RequestHeader(HttpHeaders.AUTHORIZATION) String jwt,
@RequestParam(defaultValue = "/customer/food-kind") String redirectURL,
HttpServletResponse response) throws IOException {
String cognitoUsername = parseJwtService.getCognitoUsernameFromJwt(jwt);
if(!joinService.checkJoinedMember(cognitoUsername)){
String email = parseJwtService.getEmailFromJwt(jwt);
String phoneNumber = parseJwtService.getPhoneNumberFromJwt(jwt);
joinService.joinMember(cognitoUsername, email, phoneNumber);
}
response.sendRedirect(redirectURL);
}
}
JWT를 해석하는 로직은 본 글쓴이는 아래와 같이 작성했다.
@Service
public class ParseJwtService {
public String getEmailFromJwt(String token){
JSONObject payloadJson = parseJwt(token);
return payloadJson.getString("email");
}
public String getCognitoUsernameFromJwt(String token){
JSONObject payloadJson = parseJwt(token);
return payloadJson.getString("cognito:username");
}
public String getPhoneNumberFromJwt(String token){
JSONObject payloadJSON = parseJwt(token);
return payloadJSON.getString("phone_number");
}
public JSONObject parseJwt(String token){
String base64Payload = token.split("\\.")[1];
byte[] decodedBytes = Base64.getDecoder().decode(base64Payload);
String payloadString = new String(decodedBytes);
return new JSONObject(payloadString);
}
}
DynamoDB 데이터 write 테스트하기
현재 어플리케이션을 구동하고 POSTMAN을 실행한다. POSTMAN에서 아래와 같이 Authorization 헤더에 cognito로부터 받은 id_token 내용을 담아서 방금 생성한 경로로 GET요청을 보낸다.
어플리케이션 코드에서는 HTTP 요청 헤더로부터 JWT를 받은 다음, 이를 파싱하여 DB에 사용자가 저장되어있는지 점검하고, 사용자 정보가 없다면, DB에 저장을 실행한다.
DynamoDB에 다음과 같이 사용자 정보가 저장된 것을 확인할 수 있다.
'Web Development > Spring' 카테고리의 다른 글
Spring Boot로 Kafka cloud(Confluent, Conduktor 등)에 연결하여 데이터 주고 받기 (Configuration 방법 2가지) (0) | 2024.02.25 |
---|---|
Spring 인터셉터에 url 경로 추가하는 방법. pathvariable도 포함하기 (0) | 2024.02.25 |
MongoDB의 Geo-Spatial query를 Spring Data mongoDB로 작성하는 법 (1) | 2024.02.12 |
Kakao developers API를 이용하여 주소로부터 좌표(위도, 경도) 추출해내기 (0) | 2024.02.11 |
Spring Boot 프로젝트 Docker image를 Docker Hub에 업로드하기 (0) | 2024.02.10 |