0. Table of Content

 

 

1. 개발 환경

  • STS 3.9.7
  • Spring Boot 2.2.6
  • Java 1.8

 

 

2. Dependency

plugins {
	id 'org.springframework.boot' version '2.2.6.RELEASE'
	id 'io.spring.dependency-management' version '1.0.9.RELEASE'
	id 'java'
}

group = 'com.jeonghyeong'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation('org.springframework.boot:spring-boot-starter-test') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}
	compile group: 'com.sun.mail', name: 'javax.mail', version: '1.6.2'
	
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
}

test {
	useJUnitPlatform()
}

 

AWS에서 제공하는 SDK를 사용해도 무방하나, SMTP를 사용하는데 있어서 가장 큰 요소로 작용한 것은 의존성의 용량이다.

AWS에서 제공하는 SDK와 SMTP를 이용하기 위한 의존성의 크기를 비교해보았다.

 

 

2.1. AWS SDK for SES Dependency

aws-java-sdk-ses의 의존성을 조사해본 결과 다음과 같았고, 이 의존성에 대한 용량을 파악해보기로 하였다.

 

aws-java-sdk-core + aws-java-sdk-ses + jmespath-java = 160B * 3 = 480B

 

 

 

2.2. SMTP를 이용하기 위한 Dependency

javax.mail의 의존성을 조사해본 결과 다음과 같았고, 이 의존성에 대한 용량을 파악해보기로 하였다.

 

javax.mail + javax.activation = 320B

 

의존성 용량에 대해서는 큰 차이는 없지만, 엄격하게 생각한다면 AWS-SDK를 사용하는 것 보다는 SMTP를 사용하는 방법이 컴파일 하는 시간을 단축시킬 수 있을 것이다.

 

 

 

 

 

3. Hands On Lab

SES를 처음 이용하게 되면 SES Sandbox가 설정되어 있다. Sandbox가 설정이 되어있으면, 다음과 같은 제약사항이 따른다.

	1. 하루 보낼 수 있는 제한이 200개로 제한
	2. 초당 발송 이메일 1개
	3. 등록된 이메일로만 이메일 전송 가능
Sandbox를 해제하기 위해서는 AWS Support Center에 case를 open하여 sandbox를 해제하여야 한다.

자세한 절차는 직접 콘솔에 들어가서 확인 후, 가이드에 따라서 절차를 진행하는 것을 권장한다.

 

 

 

 

먼저, 다음과 같이 SES console에서 이메일을 등록하고, 이메일 인증을 거쳐야 Amazon SES를 이용가능하다.

 

 

 

 

이메일 인증 절차를 진행하게 되면, 등록한 이메일 주소로 다음과 같은 메일이 발송된다. 메일에 첨부된 verification request link에 대해 http request를 보내면 최종적으로

SES console의 email verification status가 verified로 바뀐다. 이제 등록한 이메일을 사용할 수 있게 되었다.

 

 

 

SMTP를 이용하기 위해서는 별도의 IAM Credential이 필요하다. 다음 화면에서 발급 절차를 진행한 후, 발급된 credential를 잘 보관하길 바란다.

 

 

 

 

download한 credential정보와 Amazon SES SMTP Server Specification을 다음과 같이 spring boot의 application.properties에 기입한다.

#SES Credentials
aws.ses.username=<YOUR_SMS_CREDENTIAL_USERNAME>
aws.ses.password=<YOUR_SMS_CREDENTIAL_PASSWORD>


#Amazon SES SMTP Server Info
aws.ses.host=email-smtp.ap-south-1.amazonaws.com
aws.ses.port=587

 

 

 

이제, Email의 정보를 담을 DTO 클래스를 다음과 같이 생성한다.

package com.jeonghyeong.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor

public class Email {
	
	private String title;
	private String content;
	private String sender;
	private String receiver;

}

 

 

 

 

실질적인 테스트 이메일 발송 워크플로우를 EmailService.java에 기입 후, @Service annotation을 기입한다.

package com.jeonghyeong.service;

import java.io.UnsupportedEncodingException;
import java.util.Properties;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import com.jeonghyeong.dto.Email;


@Service
public class EmailService {
	
	@Value("${aws.ses.port}")
	private String smtpPort;
	
	@Value("${aws.ses.host}")
	private String sesHost;
	
	@Value("${aws.ses.username}")
	private String sesUsername;
	
	@Value("${aws.ses.password}")
	private String sesPassword;
	

	public void send(Email email) throws MessagingException, UnsupportedEncodingException {
		
		
        Properties props = System.getProperties();
    	props.put("mail.transport.protocol", "smtp");
    	props.put("mail.smtp.port", smtpPort); 
    	props.put("mail.smtp.starttls.enable", "true");
    	props.put("mail.smtp.auth", "true");
    	
    	Session session = Session.getDefaultInstance(props);

    	
    	System.out.println(email.getSender());
        MimeMessage msg = new MimeMessage(session);
        msg.setFrom(new InternetAddress(email.getSender(),"<EMAIL_SENDER_NICKNAME>"));
        msg.setRecipient(Message.RecipientType.TO, new InternetAddress(email.getReceiver()));
        msg.setSubject(email.getTitle());
        msg.setContent(email.getContent(),"text/html");
        
        //msg.setHeader("X-SES-CONFIGURATION-SET", "ConfigSet");
        
        Transport transport = session.getTransport();
        
        try{
            transport.connect(sesHost, sesUsername, sesPassword);	
            transport.sendMessage(msg, msg.getAllRecipients());

        }
        catch (Exception ex) {
            System.out.println("Error message: " + ex.getMessage());
        }
        finally{
            transport.close();
        }
	}
}

 

 

 

 

 

Service를 외부에 노출시키는 Rest Controller를 다음과 같이 작성해준다.

package com.jeonghyeong.controller;

import java.io.UnsupportedEncodingException;
import javax.mail.MessagingException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.jeonghyeong.dto.Email;
import com.jeonghyeong.service.EmailService;

@RestController 
public class EmailController {
	
	@Autowired
	private EmailService emailService;
	
	
	@RequestMapping("/email")
	public String emailSender(@RequestParam("title") String title, @RequestParam("content") String content, @RequestParam("sender")String sender,
			@RequestParam("receiver")String receiver) throws UnsupportedEncodingException, MessagingException {
		Email email = new Email(title, content, sender, receiver);
		
		try {
			emailService.send(email);
		}catch (Exception e) {
			System.out.println(e);
		}
			
		return "Sending Email is success, Please Check your Email.";
	}
}

 

 

위 코드를 작성한 뒤,

localhost:8080/email?title=<YOUR_EMAIL_TITLE>&content=<YOUR_EMAIL_CONTENT>&sender=<REGISTERED_EMAIL_ADDRESS_IN_SES>&receiver=<EMAIL_RECEIVER_ADDRESS>

를 호출하면 다음과 같은 이메일을 받을 수 있다.

 

 

 

 

4. Source Code

 

1. Secondary Index를 사용하는 이유

 

다음과 같은 은행 계좌를 관리하는 테이블을 생각해보자.

 

OriginCountry가 Germany인 모든 데이터를 불러온다고 가정하자. Query를 통해서 불러오기 위해선 AccountID와 CreationDate을 알고 있는 상황이어야 하지만

실제 Query상황에서는 이를 알 수 있는 방법이 없기 때문에 scan을 사용해야한다. scan을 사용하면 모든 데이터에 대해 검색을 실행하기 때문에 그만큼 실행속도가 느리다.

 

 

 

 

그러나 GSI를 사용하게 되면 다음과 같이 Primary Key를 원하는대로 바꿔서 테이블을 새로 생성 할 수 있다.

 

GSI Table(우측 테이블)을 생성하면서 기존 OriginCountry 속성을 Partition Key로 설정하였다.

기존 테이블에서 scan으로 처리하였던 요청을 query의 "KeyCondition" 함수를 이용하여 "OriginCountry eq Germany" 조건을 이용하게 되면 모두다 읽어버리는 scan보다 Read Capacity가 적게 들게된다.

 

출처 : https://www.youtube.com/watch?v=ihMOlb8EZKE

 

 

 

2. Secondary Index의 제약사항

 

다음 링크를 참조 바랍니다.

https://docs.aws.amazon.com/ko_kr/amazondynamodb/latest/developerguide/Limits.html#limits-api [DynamoDB의 제한 값]

 

 

 

 

3. Secondary Index 설계 원칙

3.1. 인덱스의 수를 최소한으로 유지한다.

  • GSI의 경우, 별도의 용량단위를 부여받기 때문에, 원본 테이블의 sync를 맞추기 위해서는 추가로 용량을 소모하며 이를 줄이는 것이 비용, Storage, I/O 관점에서 효율적이다.
  • 그러므로, 자주 query가 이루어지지 않는 속성에 대해서는 Secondary Index를 생성하지 않는다

 

3.2. LSI 이용시 index에 없는 속성을 projection하지 않는다.

  • LSI의 경우, LSI테이블에 존재하지 않고 원본 테이블에 존재하는 속성에 대해서 쿼리가 가능하다.
  • 다음 다이어그램은 위 경우를 모식화한 것이다.

 

  • LSI에 존재하지 않는 속성에 대해 query를 하게 되면 원본 테이블에서 해당 속성에 대해 찾게 된다.
  • 그 결과, 그만큼 지연시간이 걸리며, row전체를 조회한 다음 projection을 실행하기 때문에 불필요한 데이터에 RCU를 소모하게 된다.
  • 결론적으로, LSI 역시 GSI처럼 필요없는 속성은 인덱스에 포함해서는 안된다.

 

3.3. Sparse Index의 활용

  • Dense Index와 Sparse Index에 대해서는 3.3.1과 3.3.2를 참고한다.
  • 밑의 인덱스 개념을 한마디로 쉽게 설명하면 다음문장과 같다.
  • "테이블에서 Partition Key다음으로 유일하고 특징을 나타내는 속성을 GSI의 Partition Key로 설정하여 Partition을 상황에 맞게 효율적으로 커스트마이징 한다."

 

3.3.1. Dense index

  • 하나의 레코드에 하나의 포인터가 할당된다.
  • 데이터의 모든 키(Partition Key, Sort Key, 일반키 등...)가 인덱스에서 표현이 가능한 형태이다.
  • 이러한 형태는 Primary Key를 이용하여 1회의 Disk I/O 만으로 원하는 레코드를 찾을 수 있는 장점이 있다.

 

 

 

 

 

 

3.3.2. Sparse index

  • Dense Index처럼 모든 키를 가지고 있기 되면 용량이 커진다.
  • 용량이 커지는 단점을 보완하기 위해 데이터 블록마다 하나의 key-pointer쌍을 가지게 하였다.
  • 이 같은 설계를 통해 여러개의 데이터를 하나의 블록으로 합침으로써 Dense Index보다 레코드의 개수를 줄일 수 있다.
  • 레코드의 개수가 줄어든 만큼 Disk I/O는 줄어들지만, 블록안의 속성에 대해 검색할 경우에는 추가적인 disk I/O를 요구할 수 있다.

 

 

 

 

 

 

 

4. 참고문헌

 

1. @Controller

 

전통적인 Spring MVC Controller는 View 기술을 이용하여 화면을 리턴하는 방식이다.

json, xml형태의 객체를 반환하기 위해서는 @ResponseBody를 사용하여 json, xml형태의 객체를 리턴 할 수 있다.

@Controller의 workflow는 다음과 같다.

 

 

위 workflow의 순서를 간략히 설명하면 다음과 같다.

  1. Client는 URL로 서비스에 Request를 보낸다.
  2. 해당 요청을 처리할 수 있는 Handler를 찾아 Mapping 하기 위해 Dispatcher Servlet가 인터셉트 한다.
  3. Mapping 된 Handler가 존재하는 Controller에서 해당 요청을 처리하고, Model and View 객체를 반환한다.
  4. Dispatcher Servlet는 반환된 객체를 client에게 전달한다.

 

 

2. @RestController

 

View를 통해서 객체를 반환하지 않으며, 이 annotation을 통해 Controller에서 직접적으로 client에게 객체를 반환할 수 있게 되었다.

또한, 위 공식 문서의 첫 문장을 보면 다음과 같은 description이 있다.

"@RestController는 @Controller와 @ResponseBody가 annotated된 편리한 anntation이다." => @RestController = @Controller + @ResponseBody

즉, @ResponseBody의 성질에 의해서 HTTP응답에 대해 return 값을 자동으로 변환까지 해준다.

Workflow는 다음과 같다. 위 @Controller과 비교해보자.

 

 

 

 

 

 

3. 참고 문헌

+ Recent posts