์ด ํ๋ก์ ํธ๋ ๋ค์ํ RESTful API๋ฅผ ์ ๊ณตํ๋ ์๋ฒ ์ ํ๋ฆฌ์ผ์ด์
์
๋๋ค.
์ฃผ์ ๊ธฐ๋ฅ์ผ๋ก๋ STT API, Redis, JWT ์ธ์ฆ, JPA(ORM)๋ฅผ ์ฌ์ฉํ ๋ฐ์ดํฐ ๊ด๋ฆฌ ๋ฑ์ด ํฌํจ๋ฉ๋๋ค.
- ์ ์ ์กฐ๊ฑด
- Java 17 ์ด์
- Spring Boot 3.0
ํ๊ฒฝ๋ณ์ ์ค์ (application.yml)
jwt:
secretKey: ${JWT_SECRET_KEY}
access:
expiration: 3600000
header: Authorization
refresh:
expiration: 1209600000
header: Authorization-refresh
oauth:
kakao:
client_id: ${OAUTH_KAKAO_CLIENT_ID}
url:
auth: https://kauth.kakao.com
api: https://kapi.kakao.com
naver:
client_id: ${OAUTH_NAVER_CLIENT_ID}
client_secret: ${OAUTH_NAVER_CLIENT_SECRET}
redirect_uri:
google:
client_id: ${OAUTH_GOOGLE_CLIENT_ID}
client_secret: ${OAUTH_GOOGLE_CLIENT_SECRET}
redirect_uri:
spring:
application:
name: otoo_java
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: ${SPRING_DATASOURCE_URL}
username: ${SPRING_DATASOURCE_USERNAME}
password: ${SPRING_DATASOURCE_PASSWORD}
jpa:
show-sql: true
hibernate:
ddl-auto: update
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.MySQLDialect
mail:
host: ${SPRING_MAIL_HOST}
port: ${SPRING_MAIL_PORT}
username: ${SPRING_MAIL_USERNAME}
password: ${SPRING_MAIL_PASSWORD}
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
ssl:
trust: ${SPRING_MAIL_HOST}
fastapi:
url: ${FASTAPI_URL}
react:
url: ${REACT_URL}
rest:
url: ${REST_URL}
data:
redis:
port: 6379
host: localhost
server:
port: 8080
management:
endpoints:
web:
exposure:
include: health,info
endpoint:
health:
show-details: always
Redis ์ค์น ๋ฐ ์คํ
https://github.com/microsoftarchive/redis/releases ์ค์น -> redis-server ์คํ
- JWT(JSON Web Token)๋ฅผ ์ฌ์ฉํ์ฌ ์ธ์ฆ ๋ฐ ๊ถํ ๋ถ์ฌ๋ฅผ ๊ตฌํํฉ๋๋ค. ์ด ๊ธฐ๋ฅ์ ์ฌ์ฉ์์ ๋ก๊ทธ์ธ ์ํ๋ฅผ ์ ์งํ๊ณ ,
๊ถํ ์๋ ์ฌ์ฉ์๋ง์ด ํน์ API์ ์ ๊ทผํ ์ ์๋๋ก ํฉ๋๋ค.
// JwtUtil.java
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtUtil {
private final PrincipalDetailsService principalDetailsService;
private static final long ACCESS_TIME = 24 * 60 * 60 * 1000L; // 1์ผ
private static final long REFRESH_TIME = 7 * 24 * 60 * 60 * 1000L; // 7์ผ
private static final String BEARER_PREFIX = "Bearer ";
@Value("${JWT_SECRET_KEY}")
private String secretKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
@PostConstruct
public void init() {
key = Keys.hmacShaKeyFor(secretKey.getBytes());
}
public String createToken(String email, String type) {
Date date = new Date();
long time = type.equals("Access") ? ACCESS_TIME : REFRESH_TIME;
Claims claims = Jwts.claims().setSubject(email).setIssuedAt(date).setExpiration(new Date(date.getTime() + time));
return Jwts.builder().setClaims(claims).signWith(key, signatureAlgorithm).compact();
}
public boolean tokenValidation(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
log.error("Invalid JWT token: {}", e.getMessage());
return false;
}
}
}
- Redis๋ฅผ ์ฌ์ฉํ์ฌ ์บ์ฑ ๊ธฐ๋ฅ์ ๊ตฌํํ์์ต๋๋ค.
์ด๋ฅผ ํตํด ๋ฐ์ดํฐ ์ ๊ทผ ์๋๋ฅผ ํฅ์์ํฌ ์ ์์ต๋๋ค.
// RedisRefreshToken.java
@NoArgsConstructor
@Getter
@RedisHash("refreshToken")
public class RedisRefreshToken {
@Id
private String id;
private String refreshToken;
@TimeToLive(unit = TimeUnit.SECONDS)
private Long expiration;
public RedisRefreshToken(String email, String refreshToken, Long expiration) {
this.id = email;
this.refreshToken = refreshToken;
this.expiration = expiration;
}
}
- ์์ฑ ํ
์คํธ ๋ณํ(STT) API๋ฅผ ์ ๊ณตํ์ฌ ์์ฑ ๋ฐ์ดํฐ๋ฅผ ํ
์คํธ๋ก ๋ณํํฉ๋๋ค.
VITO API๋ฅผ ์ฌ์ฉํ์ฌ ์์ฑ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํฉ๋๋ค.
// SttService.java
@Slf4j
@Service
@RequiredArgsConstructor
public class SttService {
@Value("${vito.client_id}")
String client_id;
@Value("${vito.client_secret}")
String client_secret;
@Value("${FASTAPI_URL}")
String fastApiUrl;
public String getAccessToken() {
WebClient webClient = WebClient.builder()
.baseUrl("https://openapi.vito.ai")
.build();
return webClient.post()
.uri("/v1/authenticate")
.retrieve()
.bodyToMono(String.class)
.block();
}
public ResponseEntity<String> transcribeFile(MultipartFile multipartFile, String usercode) {
String accessToken = getAccessToken();
WebClient webClient = WebClient.builder()
.baseUrl("https://openapi.vito.ai/v1")
.build();
String response = webClient.post()
.uri("/transcribe")
.bodyValue(multipartFile.getBytes())
.retrieve()
.bodyToMono(String.class)
.block();
log.info("STT Response: {}", response);
return ResponseEntity.ok(response);
}
}
- Spring Security์ JWT๋ฅผ ์ฌ์ฉํ์ฌ ๋ณด์ ๊ธฐ๋ฅ์ ์ค์ ํ์์ต๋๋ค.
์ด๋ฅผ ํตํด API ์์ฒญ์ ๋ํ ์ธ์ฆ ๋ฐ ๊ถํ ๋ถ์ฌ๋ฅผ ๊ตฌํํฉ๋๋ค.
// JwtAuthFilter.java
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String accessToken = jwtUtil.getHeaderToken(request, "Access");
if (accessToken != null && jwtUtil.tokenValidation(accessToken)) {
Authentication authentication = jwtUtil.createAuthentication(jwtUtil.getEmailFromToken(accessToken));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}
- JPA๋ฅผ ์ฌ์ฉํ์ฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ํธ์์ฉํ๋ ORM(Object-Relational Mapping)์ ๊ตฌํํ์์ต๋๋ค.
์ด๋ฅผ ํตํด ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ฒด๋ก ๋ค๋ฃจ๊ณ , ๋ค์ํ ์ฟผ๋ฆฌ๋ฅผ ์ฝ๊ฒ ์์ฑํ ์ ์์ต๋๋ค.
// Repository.java
@Repository
public interface SttTalksRepository extends JpaRepository<SttTalks, Long> {
}
