이 글에서는 RedisRepository가 아닌, RedisTemplate를 사용한 코드를 포스팅합니다.

 

1
2
3
4
5
6
7
8
// maven
<dependency>    
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
 
// gradle
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
cs

먼저 redis의 의존성을 추가합니다.

 

1
2
3
4
spring:
  redis:
    host: localhost
    port: 6379
cs

그 다음은 yml에 redis의 host와 port를 적어줍니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
 
@Configuration
public class RedisConfig {
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
 
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(host, port);
    }
}
 
cs

redis 사용을 위한 configuration입니다.

host와 port는 yml에 작성한 값들을 가져옵니다.

하지만 SpringBoot 2.0부터는 auto-configuration으로 위에 작성한 RedisConnectionFactory나 RedisTemplate와 같은 것들이 자동으로 생성된다고 합니다.

따라서 SpringBoot 2.0 이상 버전을 사용하신다면 RedisConfig는 생략하셔도 됩니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
 
import java.time.Duration;
 
@Service
@RequiredArgsConstructor
@Transactional
public class RedisService {
    private final RedisTemplate<StringString> redisTemplate;
 
    public String getRedisTemplateValue(String key) {
        return redisTemplate.opsForValue().get(key);
    }
 
    public void deleteRedisTemplateValue(String key) {
        redisTemplate.delete(key);
    }
 
    public void setRedisTemplate(String key, String value, long time) {
        if (getRedisTemplateValue(key) != null) {
            deleteRedisTemplateValue(key);
        }
 
        Duration expiredDuration = Duration.ofMillis(time);
        redisTemplate.opsForValue().set(key, value, expiredDuration);
    }
}
 
cs

Redis의 기능을 담은 Service입니다.

위에서 언급한것처럼 auto-configuration으로 인해 RedisTemplate이 자동으로 생성되었기 때문에 바로 사용할 수 있습니다.

지금은 제가 문자열을 사용하기 때문에 value를 String으로 입력했지만 Object를 하셔도 됩니다.

 

set을 이용하여 <key, value> 쌍의 데이터를 저장합니다.

저장하기 전에 getRedisTemplateValue에서 key로 데이터를 검색하고, 존재한다면 deleteRedisTemplate에서 삭제부터 진행합니다.

만료 시간을 매개변수로 같이 보내서 지정할 수 있습니다.

이 때는 해당 시간이 종료되면 자동으로 삭제됩니다.

만료 시간을 매개변수로 보내지 않을 경우에는 해당 데이터가 삭제되지 않습니다.

 

세팅이 끝났으니 테스트를 해봐야겠네요.

위 명령어를 통해 redis에 접속할 수 있습니다.

여기서 -h는 호스트를 나타내며, -p는 포트번호입니다.

default는 127.0.0.1:6379 (localhost:6379)이니 localhost에서 작업하시는 분이라면 생략하셔도 됩니다.

 

그리고 위 명령어로 redis가 비어있는 것을 확인합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import com.example.test.global.redis.RedisService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
 
@SpringBootTest
public class RedisTest {
    private final long expiredTime = 60 * 1000;
 
    @Autowired
    private RedisService redisService;
 
    @Test
    void setRedis() {
        String key = "key:emoney96";
        String value = "redis test value";
 
        redisService.setRedisTemplate(key, value, expiredTime);
    }
}
 
cs

테스트 코드를 간단하게 작성해봤습니다.

key, value, expiredTime까지 값을 넣어주시고, key가 "key:emoney96"이고 만료 시간이 1분인 데이터를 redis에 저장합니다.

여기서 expiredTime은 ms(밀리 초) 단위입니다.

이 테스트를 실행하여 redis에 데이터를 넣어주겠습니다.

 

일단 테스트는 성공했고

데이터도 들어간 것을 확인할 수 있습니다.

만료 시간을 1분으로 등록했기 때문에 1분이 지나면 자동으로 사라집니다.

그리고 get key 명령어를 통해 해당 데이터의 value를 확인할 수 있습니다.

그리고 ttl 명령어로 해당 데이터의 유효 시간을 확인할 수 있습니다.

단위는 초 단위이며, 위처럼 양의 정수는 남은 시간을 뜻합니다.

만료 시간을 설정하지 않았을 경우 ttl 값은 -1입니다.

해당 데이터가 삭제됐거나 존재하지 않을 경우 ttl 값은 -2입니다.

'Spring' 카테고리의 다른 글

필드 주입 vs 생성자 주입  (0) 2022.10.31
SpringBoot OAuth 적용 [Naver - 2]  (0) 2022.10.18
SpringBoot OAuth 적용 [Naver - 1]  (0) 2022.10.17
SpringBoot OAuth 적용 [Kakao - 2]  (0) 2022.10.17
SpringBoot OAuth 적용 [Kakao - 1]  (0) 2022.10.14

우선 Spring의 의존성 주입 방법은 세 가지입니다.

  1. 필드 주입
  2. setter 주입
  3. 생성자 주입

 

1
2
3
4
5
@Service
public class ProblemService {
    @Autowired
    private ProblemRepository problemRepository;
}
cs

먼저 필드 주입입니다.

필드 바로 위에 @Autowired를 붙입니다.

 

1
2
3
4
5
6
7
8
9
@Service
public class ProblemService {
    private ProblemRepository problemRepository;
 
    @Autowired
    public void setProblemRepository(ProblemRepository problemRepository) {
        this.problemRepository = problemRepository;
    }
}
cs

다음은 setter 주입입니다.

setter 메소드 바로 위에 @Autowired를 붙입니다.

 

1
2
3
4
5
6
7
8
9
@Service
public class ProblemService {
    private final ProblemRepository problemRepository;
 
    @Autowired
    public ProblemService(ProblemRepository problemRepository) {
        this.problemRepository = problemRepository;
    }
}
cs

마지막 생성자 주입입니다.

생성자를 통해서 의존성을 주입하고, @Autowired를 붙입니다. (Spring 4.3버전 부터는 어노테이션은 생략해도 됩니다.)

1
2
3
4
5
@Service
@RequiredArgsConstructor
public class ProblemService {
    private final ProblemRepository problemRepository;
}
cs

그리고 lombok에서 제공하는 @RequiredArgsConstructor 어노테이션을 사용할 수 있습니다.

이러면 코드가 없더라도 생성자 주입을 자동으로 해줍니다.

 

저는 과거에 @Autowired를 주로 사용했었습니다.

단지 간단하고 편하다는 이유만으로 사용을 했으며, 이 방법의 문제점에 대해 생각하지 않았습니다.

 

현재, Spring에서는 "생성자 주입" 방법을 권장한다고 합니다.

필드 주입의 단점

우선 이용한 필드 주입은 @Autowired 어노테이션으로 의존성 주입을 남발할 수 있습니다.

그로 인해 생성자의 매개변수도 많아지고, 단일 책임 원칙에 위배될 수 있다고 합니다.

또한 final 사용이 불가능해서 변할 수 있는 객체 (mutable)라는 단점도 있습니다.

 

setter 주입의 단점

setter 메소드는 public으로 열려있어 객체가 변경될 가능성이 존재합니다.

실제로 변경이 있는 경우는 드물다고는 하지만 가능성이 있다는 것부터 단점입니다.

 

객체의 불변성 확보

필드 주입, setter 주입 모두 객체가 변할 수 있다고 언급하였습니다.

생성자 주입은 final 사용이 가능하고, 객체 생성 시 한 번 호출되므로 객체의 불변성을 확보할 수 있습니다.

@RequiredArgsConstructor 어노테이션은 final을 강제합니다.

객체가 final으로 생성되기 때문에 클래스 생성 시 초기값이 세팅이 되고, 변하지 않습니다. (immutable)

 

결론은 필드, setter 주입의 단점을 보완하는 생성자 주입을 쓰자였습니다.

'Spring' 카테고리의 다른 글

SpringBoot Redis 연동  (0) 2022.12.27
SpringBoot OAuth 적용 [Naver - 2]  (0) 2022.10.18
SpringBoot OAuth 적용 [Naver - 1]  (0) 2022.10.17
SpringBoot OAuth 적용 [Kakao - 2]  (0) 2022.10.17
SpringBoot OAuth 적용 [Kakao - 1]  (0) 2022.10.14

https://emoney96.tistory.com/384

 

SpringBoot OAuth 적용 [Naver - 1]

카카오에 이어 네이버 로그인입니다. 방법은 카카오와 비슷하며, 로직은 동일하다고 볼 수 있습니다. 우선 naver developers 페이지에 들어가서 네이버 계정으로 로그인합니다. 그 다음 화면 상단의

emoney96.tistory.com

[Naver - 1]에서 이어지는 글입니다.

Naver developers에서 애플리케이션을 등록하고, SpringBoot에서 의존성 및 yml 작성까지 완료했습니다.

이제 코드작성을 하겠습니다.

코드는 카카오 OAuth와 거의 동일하다고 볼 수 있습니다.

 

우선 개발 가이드 및 API 명세를 참고하시는게 많은 도움이됩니다.

https://developers.naver.com/docs/login/devguide/devguide.md

 

네이버 로그인 개발가이드 - LOGIN

네이버 로그인 개발가이드 1. 개요 4,200만 네이버 회원을 여러분의 사용자로! 네이버 회원이라면, 여러분의 사이트를 간편하게 이용할 수 있습니다. 전 국민 모두가 가지고 있는 네이버 아이디

developers.naver.com

https://developers.naver.com/docs/login/api/api.md

 

네이버 로그인 API 명세 - LOGIN

네이버 로그인 API 명세 네이버 로그인 API는 네이버 로그인 인증 요청 API, 접근 토큰 발급/갱신/삭제 요청API로 구성되어 있습니다. 네이버 로그인 인증 요청 API는 여러분의 웹 또는 앱에 네이버

developers.naver.com

 

로그인 URI

1
2
3
4
5
6
7
8
9
10
11
// Controller
@GetMapping("/naver/page")
public ResponseEntity<String> getNaverLoginPage() {
    return new ResponseEntity<>(userService.getNaverLoginPage(), HttpStatus.OK);
}
 
// Service
public String getNaverLoginPage() {
    String state = new BigInteger(130new SecureRandom()).toString();
    return naverAuthorizationUri + "?client_id=" + naverClientId + "&redirect_uri=" + naverRedirectUri + "&response_type=code&state=" + state;
}
cs

먼저 네이버 로그인 페이지를 가져오는 api입니다.

Service에서 URI를 반환하며, state라는 난수를 추가합니다.

 

로그인 요청

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@GetMapping("/naver/login")
public ResponseEntity<UserGetRes> getNaverTokensAndSignUpOrLogin(@RequestParam String code, HttpServletResponse response) {
    // code를 이용하여 accessToken, refreshToken을 받아온다.
    HashMap<StringString> tokens = userService.getSocialTokens(code);
 
    UserInsertReq userInsertReq = userService.getSocialUserInfo(tokens);
 
    User findUser = userService.findUserByEmail(userInsertReq.getEmail());
    if (findUser == null) {
        // 유저 정보가 없으므로 회원 가입 진행
        return new ResponseEntity<>(userService.insertUser(userInsertReq, response), HttpStatus.CREATED);
    } else {
        // 유저 정보가 있으므로 토큰 갱신 후 로그인
        return new ResponseEntity<>(userService.updateUser(findUser.getEmail(), tokens, response), HttpStatus.OK);
    }
}
cs

Controller입니다.

네이버 로그인 시 나오는 코드로 accessToken, refreshToken을 발급 받습니다.

이후 토큰을 이용하여 네이버 api에 회원 정보를 요청하고, 해당 유저가 신규유저인지, 기존유저인지 확인합니다.

신규유저라면 회원가입을, 기존유저라면 정보 갱신 후 로그인을 진행합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public HashMap<StringString> getSocialTokens(String code) {
    String accessToken = "";
    String refreshToken = "";
 
    HashMap<StringString> keyAndValues = new HashMap<>();
 
    keyAndValues.put("tokenUri", naverTokenUri);
    keyAndValues.put("authenticationMethod", naverAuthenticationMethod);
    keyAndValues.put("grantType", naverGrantType);
    keyAndValues.put("clientId", naverClientId);
    keyAndValues.put("clientSecret", naverClientSecret);
    keyAndValues.put("redirectUri", naverRedirectUri);
    keyAndValues.put("state"new BigInteger(130new SecureRandom()).toString());
 
    try {
        URL url = new URL(keyAndValues.get("tokenUri"));
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
 
        conn.setRequestMethod(keyAndValues.get("authenticationMethod"));
        conn.setDoOutput(true);
 
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(conn.getOutputStream()));
        StringBuilder sb = new StringBuilder();
        sb.append("grant_type=" + keyAndValues.get("grantType"));
        sb.append("&client_id=" + keyAndValues.get("clientId"));
        sb.append("&client_secret=" + keyAndValues.get("clientSecret"));
        sb.append("&redirect_uri=" + keyAndValues.get("redirectUri"));
        sb.append("&code=" + code);
        sb.append("&state=" + keyAndValues.get("state"));
        bw.write(sb.toString());
        bw.flush();
 
        BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
        String line = "";
        String result = "";
 
        while ((line = br.readLine()) != null) {
            result += line;
        }
 
        JsonParser parser = new JsonParser();
        JsonElement element = parser.parse(result);
 
        accessToken = element.getAsJsonObject().get("access_token").getAsString();
        refreshToken = element.getAsJsonObject().get("refresh_token").getAsString();
 
        br.close();
        bw.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
 
    HashMap<StringString> tokens = new HashMap<>();
    tokens.put("accessToken", accessToken);
    tokens.put("refreshToken", refreshToken);
 
    return tokens;
}
cs

카카오, 네이버 oauth를 함께 사용하고 있고, 로직이 동일하기에 한 메소드에서 처리하려고 했고, HashMap을 사용하였습니다.

하나만 쓰신다면 HashMap을 사용하실 필요는 없습니다.

 

위 로직은 Service 부분이고, code로 token을 요청해서 받는 부분으로 tokenUri에 필수 request Parameter를 추가해서 요청합니다.

https://developers.naver.com/docs/login/devguide/devguide.md#3-4-4-%EC%A0%91%EA%B7%BC-%ED%86%A0%ED%81%B0-%EB%B0%9C%EA%B8%89-%EC%9A%94%EC%B2%AD 관련 문서입니다.

그 다음 발급받은 accessToken, refreshToken을 HashMap에 저장합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public UserInsertReq getSocialUserInfo(HashMap<StringString> tokens) {
    String userInfoUri = "";
    String authenticationMethod = "";
 
    userInfoUri = naverUserInfoUri;
    authenticationMethod = naverAuthenticationMethod;
 
    JsonElement element = null;
 
    try {
        URL url = new URL(userInfoUri);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
 
        conn.setRequestMethod(authenticationMethod);
        conn.setDoOutput(true);
        conn.setRequestProperty("Authorization", tokenType + " " + tokens.get("accessToken"));
 
        BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
        String line = "";
        String result = "";
 
        while ((line = br.readLine()) != null) {
            result += line;
        }
 
        JsonParser parser = new JsonParser();
        element = parser.parse(result);
 
        br.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
 
    return UserInsertReq.of(element, tokens);
}
cs

발급 받은 토큰을 이용하여 유저 정보를 요청합니다.

https://developers.naver.com/docs/login/devguide/devguide.md#3-4-5-%EC%A0%91%EA%B7%BC-%ED%86%A0%ED%81%B0%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-%ED%94%84%EB%A1%9C%ED%95%84-api-%ED%98%B8%EC%B6%9C%ED%95%98%EA%B8%B0 관련 문서입니다.

필수 항목이나 사용자가 동의한 항목을 받아올 수 있습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UserInsertReq {
    private String email;
    private String nickname;
    private String accessToken;
    private String refreshToken;
 
    public static UserInsertReq of(JsonElement element, HashMap<StringString> tokens) {
        return UserInsertReq.builder()
                .email(element.getAsJsonObject().get("response").getAsJsonObject().get("email").getAsString())
                .nickname(element.getAsJsonObject().get("response").getAsJsonObject().get("nickname").getAsString())
                .accessToken(tokens.get("accessToken"))
                .refreshToken(tokens.get("refreshToken"))
                .build();
    }
}
cs

이제 받아온 정보를 Dto에 넣어줍니다.

 

1
2
3
4
5
public User findUserByEmail(String email) {
    Optional<User> optionalUser = userRepository.findUserByEmail(email);
    if (optionalUser.isPresent()) return optionalUser.get();
    else return null;
}
cs

그 다음은 email로 가입된 유저인지 확인합니다.

기존유저라면 유저객체를, 신규회원이라면 null을 리턴합니다.

Repository는 JPA를 이용하였고, 코드는 생략합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
public UserGetRes insertUser(UserInsertReq userInsertReq, HttpServletResponse response) {
    addCookie(response, userInsertReq.getAccessToken());
 
    return UserGetRes.of(userRepository.save(new User(userInsertReq)));
}
 
public void addCookie(HttpServletResponse response, String accessToken) {
    Cookie cookie = new Cookie("accessToken", accessToken);
    cookie.setMaxAge(3600);
    cookie.setPath("/");
    response.addCookie(cookie);
}
cs

null을 리턴하여 신규회원인 것이 확인되었으면 insert를 진행합니다.

로그인 처리까지 하기위해 accessToken을 쿠키에 넣어줍니다.

유효시간은 초 단위이며, 1시간으로 지정했습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
public UserGetRes updateUser(String email, HashMap<StringString> tokens, HttpServletResponse response) {
    Optional<User> optionalUser = userRepository.findById(email);
    if (!optionalUser.isPresent())
        return null;
 
    User user = optionalUser.get();
    user.setAccessToken(tokens.get("accessToken"));
    user.setRefreshToken(tokens.get("refreshToken"));
    addCookie(response, user.getAccessToken());
 
    return UserGetRes.of(userRepository.save(user));
}
cs

유저객체를 리턴하여 기존회원인 것이 확인되었으면 update를 진행합니다.

새롭게 발급받은 accessToken, refreshToken만 갱신하고 로그인 처리를 위해 accessToken을 쿠키에 넣어줍니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UserGetRes {
    private String email;
    private String nickname;
    private String accessToken;
    private String refreshToken;
 
    public static UserGetRes of(User user) {
        return UserGetRes.builder()
                .email(user.getEmail())
                .nickname(user.getNickname())
                .accessToken(user.getAccessToken())
                .refreshToken(user.getRefreshToken())
                .build();
    }
}
cs

로그인이 되었을 때 프론트에 넘겨주는 데이터를 담은 ResponseDto입니다.

RequestDto와 같은 변수를 담고있어서 중복되는 문제가 있지만 비밀번호처럼 보안이 필요한 변수가 있으면 다르게 나오지 않을까 생각합니다.

 

신규 회원
기존 회원

분명 신규회원, 기존회원이라는 차이가 있지만 accessToken, refreshToken은 동일합니다.

네이버에서 토큰 발급은 유효기간이 끝날때까지 동일한 토큰을 발급하는 것 같습니다.

따라서 이 방법보다는 refreshToken으로 accessToken을 재발급 받는 것이 좋지 않을까 생각은 듭니다.

https://developers.naver.com/docs/login/devguide/devguide.md#5-1-2-%EA%B0%B1%EC%8B%A0-%ED%86%A0%ED%81%B0%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC 토큰 갱신 관련 링크입니다.

'Spring' 카테고리의 다른 글

SpringBoot Redis 연동  (0) 2022.12.27
필드 주입 vs 생성자 주입  (0) 2022.10.31
SpringBoot OAuth 적용 [Naver - 1]  (0) 2022.10.17
SpringBoot OAuth 적용 [Kakao - 2]  (0) 2022.10.17
SpringBoot OAuth 적용 [Kakao - 1]  (0) 2022.10.14

카카오에 이어 네이버 로그인입니다.

 

방법은 카카오와 비슷하며, 로직은 동일하다고 볼 수 있습니다.

 

우선 naver developers 페이지에 들어가서 네이버 계정으로 로그인합니다.

그 다음 화면 상단의 Application -> 내 애플리케이션을 누릅니다.

그러면 위 화면이 나옵니다.

여기서 "Application 등록" 버튼을 눌러서 애플리케이션을 생성합니다.

 

여기서 애플리케이션 이름과 사용할 API를 선택합니다.

이 글의 컨텐츠는 네이버 로그인이므로 네이버 로그인을 선택합니다.

네이버의 개인정보는 카카오처럼 검수과정을 거치지 않아도 이름, 이메일 등 많은 정보를 필수동의로 지정할 수 있습니다.

사용할 동의항목을 설정합니다. 저는 이메일과 별명을 선택했습니다.

 

그리고 로그인 서비스 환경을 선택합니다.

저는 PC 웹을 선택하였고, 서비스 URL과 Callback URL을 작성합니다.

서비스 URL은 말 그대로 메인 URL이며, Callback URL은 로그인 성공 시 이동할 주소를 작성합니다.

끝났다면 아래의 등록하기 버튼을 누릅니다.

 

그러면 애플리케이션이 생성되었고, 먼저 Key가 보입니다.

 

 

이제 SpringBoot로 넘어갑니다.

https://developers.naver.com/docs/login/devguide/devguide.md

 

네이버 로그인 개발가이드 - LOGIN

네이버 로그인 개발가이드 1. 개요 4,200만 네이버 회원을 여러분의 사용자로! 네이버 회원이라면, 여러분의 사이트를 간편하게 이용할 수 있습니다. 전 국민 모두가 가지고 있는 네이버 아이디

developers.naver.com

https://developers.naver.com/docs/login/api/api.md

 

네이버 로그인 API 명세 - LOGIN

네이버 로그인 API 명세 네이버 로그인 API는 네이버 로그인 인증 요청 API, 접근 토큰 발급/갱신/삭제 요청API로 구성되어 있습니다. 네이버 로그인 인증 요청 API는 여러분의 웹 또는 앱에 네이버

developers.naver.com

로그인 API 명세 및 개발 가이드 문서를 참고해주시면 됩니다.

 

# gradle
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

# Maven
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

먼저 OAuth를 위한 의존성을 추가합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring:
  security:
    oauth2:
      client:
        provider:
          naver:
            authorization-uri: https://nid.naver.com/oauth2.0/authorize # 네이버 로그인 요청 uri
            token-uri: https://nid.naver.com/oauth2.0/token # 토큰을 발급받기 위한 uri
            user-info-uri: https://openapi.naver.com/v1/nid/me # 유저 정보를 받아오기 위한 uri
        registration:
          naver:
            client-id: # Client ID
            client-secret: # Client Secret
            client-authentication-method: POST
            authorization-grant-type: authorization_code
            redirect-uri: # Callback URL
            scope:
              - email
              - nickname
cs

그 다음은 yml에 필수 정보를 넣어줍니다.

여기 넣은 정보들은 Java 코드에서 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Value("${spring.security.oauth2.client.registration.naver.client-id}")
private String naverClientId;
@Value("${spring.security.oauth2.client.registration.naver.client-secret}")
private String naverClientSecret;
@Value("${spring.security.oauth2.client.registration.naver.client-authentication-method}")
private String naverAuthenticationMethod;
@Value("${spring.security.oauth2.client.registration.naver.authorization-grant-type}")
private String naverGrantType;
@Value("${spring.security.oauth2.client.registration.naver.redirect-uri}")
private String naverRedirectUri;
@Value("${spring.security.oauth2.client.provider.naver.authorization-uri}")
private String naverAuthorizationUri;
@Value("${spring.security.oauth2.client.provider.naver.token-uri}")
private String naverTokenUri;
@Value("${spring.security.oauth2.client.provider.naver.user-info-uri}")
private String naverUserInfoUri;
@Value("Bearer")
private String tokenType;
cs

 

 

코드작성 부분은 다음 글에서 다루도록 하겠습니다.

'Spring' 카테고리의 다른 글

필드 주입 vs 생성자 주입  (0) 2022.10.31
SpringBoot OAuth 적용 [Naver - 2]  (0) 2022.10.18
SpringBoot OAuth 적용 [Kakao - 2]  (0) 2022.10.17
SpringBoot OAuth 적용 [Kakao - 1]  (0) 2022.10.14
JPA order by rand() limit  (0) 2022.10.13

https://emoney96.tistory.com/381

 

SpringBoot OAuth 적용 [Kakao - 1]

제가 경험했던 OAuth를 정리하기 위해 포스팅합니다. 카카오, 네이버 로그인을 시도해보았고, 차례대로 포스팅하겠습니다. 먼저 카카오입니다. 우선 kakao developers 페이지에 들어가서 회원가입/로

emoney96.tistory.com

[Kakao - 1]에서 이어지는 글입니다.

이전까지 kakao developers에서 애플리케이션 등록, SpringBoot에서 의존성 및 yml 작성까지 진행하였습니다.

이제 setting이 끝났으니 코드를 작성하겠습니다.

 

 

지금부터 나오는 API 요청들은 문서를 참고하시면 이해하기 더 수월합니다!

https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

 

로그인 URI

1
2
3
4
5
6
7
8
9
10
// Controller
@GetMapping("/kakao/page")
public ResponseEntity<String> getKakaoLoginPage() {
    return new ResponseEntity<>(userService.getKakaoLoginPage(), HttpStatus.OK);
}
 
// Service
public String getKakaoLoginPage() {
    return kakaoAuthorizationUri + "?client_id=" + kakaoClientId + "&redirect_uri=" + kakaoRedirectUri + "&response_type=code";
}
cs

먼저 기본적인 카카오 로그인 페이지를 가져오는 api입니다.

Service에서 URI를 반환하여 보내줍니다.

 

로그인 요청

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@GetMapping("/kakao/login")
public ResponseEntity<UserGetRes> getKakaoTokensAndSignUpOrLogin(@RequestParam String code, HttpServletResponse response) {
    // code를 이용하여 accessToken, refreshToken을 받아온다.
    HashMap<StringString> tokens = userService.getSocialTokens(code);
 
    UserInsertReq userInsertReq = userService.getSocialUserInfo(tokens);
 
    User findUser = userService.findUserByEmail(userInsertReq.getEmail());
    if (findUser == null) {
        // 유저 정보가 없으므로 회원 가입 진행
        return new ResponseEntity<>(userService.insertUser(userInsertReq, response), HttpStatus.CREATED);
    } else {
        // 유저 정보가 있으므로 토큰 갱신 후 로그인
        return new ResponseEntity<>(userService.updateUser(findUser.getEmail(), tokens, response), HttpStatus.OK);
    }
}
cs

먼저 Controller입니다.

카카오 로그인 시 나오는 코드로 accessToken, refreshToken을 발급 받습니다.

이후 카카오 api에서 유저 정보를 조회해서 가져오고,

해당 유저가 이미 가입되어 있다면 update로 토큰을 갱신하고, 신규 회원이라면 insert로 회원가입 처리를 합니다.

물론 동시에 로그인까지 완료됩니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public HashMap<StringString> getSocialTokens(String code) {
    String accessToken = "";
    String refreshToken = "";
 
    HashMap<StringString> keyAndValues = new HashMap<>();
 
    keyAndValues.put("tokenUri", kakaoTokenUri);
    keyAndValues.put("authenticationMethod", kakaoAuthenticationMethod);
    keyAndValues.put("grantType", kakaoGrantType);
    keyAndValues.put("clientId", kakaoClientId);
    keyAndValues.put("clientSecret", kakaoClientSecret);
    keyAndValues.put("redirectUri", kakaoRedirectUri);
 
    try {
        URL url = new URL(keyAndValues.get("tokenUri"));
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
 
        conn.setRequestMethod(keyAndValues.get("authenticationMethod"));
        conn.setDoOutput(true);
 
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(conn.getOutputStream()));
        StringBuilder sb = new StringBuilder();
        sb.append("grant_type=" + keyAndValues.get("grantType"));
        sb.append("&client_id=" + keyAndValues.get("clientId"));
        sb.append("&client_secret=" + keyAndValues.get("clientSecret"));
        sb.append("&redirect_uri=" + keyAndValues.get("redirectUri"));
        sb.append("&code=" + code);
 
        bw.write(sb.toString());
        bw.flush();
 
        BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
        String line = "";
        String result = "";
 
        while ((line = br.readLine()) != null) {
            result += line;
        }
 
        JsonParser parser = new JsonParser();
        JsonElement element = parser.parse(result);
 
        accessToken = element.getAsJsonObject().get("access_token").getAsString();
        refreshToken = element.getAsJsonObject().get("refresh_token").getAsString();
 
        br.close();
        bw.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
 
    HashMap<StringString> tokens = new HashMap<>();
    tokens.put("accessToken", accessToken);
    tokens.put("refreshToken", refreshToken);
 
    return tokens;
}
cs

HashMap을 사용한건 카카오와 네이버 oauth를 같이 사용하기 위함이었지만 하나만 쓰신다면 HashMap을 안쓰셔도 됩니다.

 

위 로직은 Service 부분이고, code로 token을 요청해서 받는 부분으로 tokenUri에 필수 request Parameter를 추가하여 요청해야 합니다.

https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-token-request 관련 문서 내용입니다.

이제 받아온 내용을 Json으로 읽어서 accessToken, refreshToken을 HashMap에 저장합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public UserInsertReq getSocialUserInfo(HashMap<StringString> tokens) {
    String userInfoUri = "";
    String authenticationMethod = "";
 
    userInfoUri = kakaoUserInfoUri;
    authenticationMethod = kakaoAuthenticationMethod;
 
    JsonElement element = null;
 
    try {
        URL url = new URL(userInfoUri);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
 
        conn.setRequestMethod(authenticationMethod);
        conn.setDoOutput(true);
        conn.setRequestProperty("Authorization", tokenType + " " + tokens.get("accessToken"));
 
        BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
        String line = "";
        String result = "";
 
        while ((line = br.readLine()) != null) {
            result += line;
        }
 
        JsonParser parser = new JsonParser();
        element = parser.parse(result);
 
        br.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
 
    return UserInsertReq.of(element, tokens);
}
cs

이제 토큰 정보를 받았으니 토큰을 이용하여 유저 정보를 요청합니다.

https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info 관련 문서 내용입니다.

필수 동의 항목이나 사용자가 동의한 항목을 받아올 수 있습니다.

위와 같이 Json을 통해 가져왔습니다.

 

이거를 Dto에 넣어줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UserInsertReq {
    private String email;
    private String nickname;
    private String accessToken;
    private String refreshToken;
 
    public static UserInsertReq of(JsonElement element, HashMap<StringString> tokens) {
        return UserInsertReq.builder()
                .email(element.getAsJsonObject().get("kakao_account").getAsJsonObject().get("email").getAsString())
                .nickname(element.getAsJsonObject().get("kakao_account").getAsJsonObject().get("profile").getAsJsonObject().get("nickname").getAsString())
                .accessToken(tokens.get("accessToken"))
                .refreshToken(tokens.get("refreshToken"))
                .build();
    }
}
cs

email, nickname같은 변수는 설정한 동의 항목에 따라 달라집니다.

1
2
3
4
5
public User findUserByEmail(String email) {
    Optional<User> optionalUser = userRepository.findUserByEmail(email);
    if (optionalUser.isPresent()) return optionalUser.get();
    else return null;
}
cs

회원 정보까지 가져왔다면 가져온 정보의 key로 가입된 유저인지 확인하기 위해 db에 접근합니다.

기존회원이라면 유저객체를, 신규회원이라면 null을 리턴합니다.

Repository는 JPA를 이용하였으며, 코드는 생략합니다.

1
2
3
4
5
6
7
8
9
10
11
12
public UserGetRes insertUser(UserInsertReq userInsertReq, HttpServletResponse response) {
   addCookie(response, userInsertReq.getAccessToken());
 
    return UserGetRes.of(userRepository.save(new User(userInsertReq)));
}
 
public void addCookie(HttpServletResponse response, String accessToken) {
    Cookie cookie = new Cookie("accessToken", accessToken);
    cookie.setMaxAge(3600);
    cookie.setPath("/");
    response.addCookie(cookie);
}
cs

null을 리턴하여 신규회원인 것이 확인되었으면 insert를 진행합니다.

그리고 로그인도 되니 accessToken을 쿠키에 넣어줍니다.

유효시간은 초 단위이며, 1시간으로 지정했습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
public UserGetRes updateUser(String email, HashMap<StringString> tokens, HttpServletResponse response) {
    Optional<User> optionalUser = userRepository.findById(email);
    if (!optionalUser.isPresent())
        return null;
 
    User user = optionalUser.get();
    user.setAccessToken(tokens.get("accessToken"));
    user.setRefreshToken(tokens.get("refreshToken"));
    addCookie(response, user.getAccessToken());
 
    return UserGetRes.of(userRepository.save(user));
}
cs

유저 객체를 리턴하여 기존회원인 것이 확인되었으면 update를 진행합니다.

새롭게 발급받은 accessToken, refreshToken만 갱신하고, update또한 로그인도 되니 토큰을 쿠키에 넣어줍니다.

쿠키에 넣는 로직은 위 코드와 동일합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UserGetRes {
    private String email;
    private String nickname;
    private String accessToken;
    private String refreshToken;
 
    public static UserGetRes of(User user) {
        return UserGetRes.builder()
                .email(user.getEmail())
                .nickname(user.getNickname())
                .accessToken(user.getAccessToken())
                .refreshToken(user.getRefreshToken())
                .build();
    }
}
cs

로그인이 되었을 때 프론트에 넘겨주는 데이터를 담은 ResponseDto입니다.

RequestDto와 같은 변수를 담고있어 Req, Res로 나눌 필요가 없긴 하지만 실제 개발에서는 Req,Res 데이터가 다르기 때문에 나눴습니다.

 

신규 회원
기존 회원

이제 로그인을 시도하면 데이터가 잘 들어가는 것을 확인할 수 있습니다.

 

 

카카오 로그인은 여기까지이며, 다음은 네이버 로그인 연동을 포스팅하겠습니다.

제가 경험했던 OAuth를 정리하기 위해 포스팅합니다.

카카오, 네이버 로그인을 시도해보았고, 차례대로 포스팅하겠습니다.

 

먼저 카카오입니다.

 

우선 kakao developers 페이지에 들어가서 회원가입/로그인을 진행합니다.

그 다음 화면 상단의 "내 애플리케이션"을 클릭하여 들어옵니다.

여기서 "애플리케이션 추가하기"를 누릅니다.

 

정보를 입력하고 저장 버튼을 누릅니다.

본인이 만든 애플리케이션이 추가가 되었으니 들어갑니다.

들어오자마자 앱 키가 뜹니다.

당연히 중요한 키들이니 노출되지 않도록 주의해야 합니다.

여기서 사용할 키는 "REST API 키" 입니다.

키 등록은 SpringBoot에서 할 예정이니 왼쪽 메뉴의 "플랫폼"으로 넘어갑니다.

 

여기서 본인의 프로젝트에 맞는 플랫폼을 등록합니다.

저는 Web을 작성하였습니다.

 

그 다음은 카카오 로그인을 활성화 합니다.

상태가 OFF인데, 눌러서 ON으로 변경합니다.

그 밑에 있는 Redirect URI는 로그인이 성공했을 때 이동할 URI를 작성해주시면 됩니다.

구분은 엔터로해서 최대 10개를 등록할 수 있습니다.

local에서 테스트하는 uri와 배포를 하셨다면 배포uri를 넣어줍니다.

그 다음 동의항목입니다.

애플리케이션에서 사용할 동의항목을 설정합니다.

하지만 카카오계정(이메일)과 같은 중요한 정보는 "필수 동의"가 안되거나 "권한 없음" 설정이 되어있는 항목이 있습니다.

이런 경우에는 "비즈니스 설정 바로가기"에 들어가서 사업자 정보를 등록해야 더 많은 권한을 받을 수 있습니다.

회사에서 진행중인 프로젝트라면 가능하겠지만 학생 또는 교육생이거나 개인 프로젝트에서는 등록하기 어렵다고 볼 수 있습니다.

 

동의항목 설정이 끝났다면 보안으로 넘어갑니다.

Client Secret 코드를 발급 받아 보안을 강화할 수 있습니다.

되도록이면 발급 받는 것을 권장합니다.

토큰 발급을 받았더라면 활성화 상태를 "사용함"으로 변경합니다.

기본값은 "사용안함"입니다.

 

 

그러면 이제 SpringBoot로 넘어갑니다.

https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

우선 REST API 요청 관련은 문서를 참고해주시면 됩니다.

 

# gradle
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

# Maven
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

먼저 OAuth를 위한 의존성을 추가합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
spring:
  security:
    oauth2:
      client:
        provider:
          kakao:
            authorization-uri: https://kauth.kakao.com/oauth/authorize # 카카오 로그인 요청 uri
            token-uri: https://kauth.kakao.com/oauth/token # 토큰을 발급받기 위한 uri
            user-info-uri: https://kapi.kakao.com/v2/user/me # 유저 정보를 받아오기 위한 uri
        registration:
          kakao:
            client-id: # REST API Key
            client-secret: # Client Secret Key
            client-authentication-method: POST
            authorization-grant-type: authorization_code
            redirect-uri: # Redirect URI
cs

그 다음은 yml파일에 필수 정보를 넣어줍니다.

여기 넣은 정보들은 Java 코드에서 아래와 같이 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Value("${spring.security.oauth2.client.registration.kakao.client-id}")
private String kakaoClientId;
@Value("${spring.security.oauth2.client.registration.kakao.client-secret}")
private String kakaoClientSecret;
@Value("${spring.security.oauth2.client.registration.kakao.client-authentication-method}")
private String kakaoAuthenticationMethod;
@Value("${spring.security.oauth2.client.registration.kakao.authorization-grant-type}")
private String kakaoGrantType;
@Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}")
private String kakaoRedirectUri;
@Value("${spring.security.oauth2.client.provider.kakao.authorization-uri}")
private String kakaoAuthorizationUri;
@Value("${spring.security.oauth2.client.provider.kakao.token-uri}")
private String kakaoTokenUri;
@Value("${spring.security.oauth2.client.provider.kakao.user-info-uri}")
private String kakaoUserInfoUri;
@Value("Bearer")
private String tokenType;
cs

 

 

글이 길어질 것 같아서 코드작성 부분은 다음 글에서 다루도록 하겠습니다.

sql에는 랜덤으로 row를 가져올 수 있는 query가 존재합니다.

"select * from word order by rand() limit 3;" 이런식으로 무작위로 3개 가져오는 쿼리도 실행이 가능합니다.

 

JPA는 메소드 이름으로 쿼리문을 실행해주는 편리한 인터페이스입니다.

"find + (Entity) + By + Column"의 형식의 메소드를 작성하면 이에 맞는 쿼리를 실행해줍니다.

친구와 미니 프로젝트를 하는 도중 랜덤 조회를 하기 위해 JPA 메소드를 작성하였습니다.

그렇게 차례대로 메소드를 써나가는데 OrderBy 뒤에 random과 관련된 단어가 보이지 않았습니다.

limit은 find 뒤에 Top, First, Last와 같은 키워드로 대체할 수 있습니다.

No property 'rand' found for type 'Word'

이렇게 어거지로 써봤지만 실행조차 되지않았습니다.

JPA 메소드에서 Order By는 컬럼명만 쓸 수 있으며, 랜덤 조회가 불가능하다는 결론이 나왔습니다.

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods

 

Spring Data JPA - Reference Documentation

Example 109. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

docs.spring.io

JPA 공식문서를 확인해도 random 관련 내용은 볼 수 없었습니다.

그리고 랜덤으로 N개를 조회하기 위해 "Top20"을 메소드에 추가하였지만 "20"이라는 고정된 값이기 때문에 변수로 바꾸려고 합니다.

 

우선 제가 생각해낸 방법은 @Query 어노테이션을 사용하는 방법입니다.

JPA 메소드 위에 @Query(value = "query string")를 붙이는 방법으로, 메소드명을 아무렇게 작성하더라고 위에 작성한 query대로 실행됩니다.

그래서 이렇게 쓰긴 했는데 에러가 발생합니다??

 

구글링으로 nativeQuery라는 것을 발견하였고,

nativeQuery = true면 SQL, nativeQuery = false면 JPQL 문법으로 구분한다고 합니다.

nativeQuery = false가 default라서 JPQL이라고 인식을 하고있지만 JPQL 문법에 맞지 않아 발생한 에러였습니다.

이제는 에러가 발생하지 않습니다.

실행 시에도 단어가 무작위로, cnt만큼 잘 나오는 것을 확인할 수 있습니다.

 

이 방법 외에도 다른 방법이 있는지 모르겠습니다.

@NamedQuery를 Entity에 붙여 사용해보았지만 @NamedQuery는 JPQL 문법만 사용이 가능한 것 같아 실패하였습니다. ㅠㅠ

 

https://whyeskang.com/258

 

Postman을 이용한 File, Dto 동시 Post요청

보통 Controller에서 Dto를 받을 때는 @RequestBody를 주로 사용합니다. 그리고 File을 받을 때는 MultipartFile 객체를 사용하며, @RequestParam을 사용합니다. 하지만 File과 Dto를 같이 받기 위해서는 @RequestPart라

whyeskang.com

작년에 이어 File, Dto 동시 요청 2탄입니다.

 

이전 글에서는 단일 File과 Dto를 동시에 요청받았습니다.

이번 글에서는 File과 Dto의 List를 Postman을 이용하여 요청하는 것을 다루려고 합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
@Data
@NoArgsConstructor
public class User {
    @Id
    String userId;
    String password;
    String profileImageUrl;
 
    public User(UserSignUpReq userSignUpReq) {
        this.userId = userSignUpReq.getUserId();
        this.password = userSignUpReq.getPassword();
    }
}
cs

User Entity입니다.

테스트 용도이니 간단하게만 작성해줍니다.

1
2
3
4
5
@Data
public class UserSignUpReq {
    String userId;
    String password;
}
cs

RequestDto 또한 간단하게 해줍니다.

 

 

1
2
3
4
5
6
7
8
9
10
@PostMapping("/signup")
public ResponseEntity<List<UserGetRes>> signUpUser(
       @RequestPart(value = "multipartFileList") List<MultipartFile> multipartFileList,
       @RequestPart(value = "userSignUpReqList") List<UserSignUpReq> userSignUpReqList) {
    return new ResponseEntity<>(
            userService.loginUser(
                    multipartFileList,
                    userSignUpReqList.stream().map(userSignUpReq -> new User(userSignUpReq)).collect(Collectors.toList())),
            HttpStatus.OK);
}
cs

그 다음 요청을 받을 Conrtoller입니다.

어노테이션은 @RequestPart로 이전과 같으며, 요청을 정상적으로 받았는지 확인하기 위해 List<ResDto>를 리턴합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Override
public List<UserGetRes> loginUser(List<MultipartFile> multipartFileList, List<User> userList) {
    try {
        String separ = File.separator;
        String today = new SimpleDateFormat("yyMMdd").format(new Date());
 
        File file = new File("");
        String rootPath = file.getAbsolutePath().split("src")[0];
        String savePath = rootPath + separ + "profileImage" + separ + today;
        if (!new File(savePath).exists()) {
            try {
                new File(savePath).mkdirs();
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
 
        int len = multipartFileList.size();
        for (int i = 0; i < len; i++) {
            // multipartFileList와 userList의 길이가 무조건 같고, 순서가 일치하다고 가정한다.
            MultipartFile multipartFile = multipartFileList.get(i);
 
            String originFileName = multipartFile.getOriginalFilename();
            String saveFileName = UUID.randomUUID().toString() + originFileName.substring(originFileName.lastIndexOf("."));
 
            String filePath = savePath + separ + saveFileName;
            multipartFile.transferTo(new File(filePath));
 
            userList.get(i).setProfileImageUrl(filePath);
        }
 
        return userRepository.saveAll(userList).stream().map(user -> UserGetRes.of(user)).collect(Collectors.toList());
    }
    catch (Exception e) {
        e.printStackTrace();
        return null;
    }
}
cs

받은 요청을 처리할 Service입니다.

File과 User List의 길이와 순서가 같다고 가정하고 구현한 코드입니다.

중요한건 "File과 User의 List를 같이 받는 것" 입니다.

파일을 지정한 경로에 저장한 후 DB에 추가하는 로직이며, 이전 글에 List만 추가되었습니다.

 

우선 단일 File, Dto 요청 시에는 Body -> form-data에서 File 업로드 및 Dto는 json 방식으로 작성하면 된다고 하였습니다.

 

 

아 맞다 contentType 추가해야하는데

 

Dto 쪽에는 contentType을 application/json으로 맞춰줘야 합니다.

 

 

List를 추가한다고 해서 크게 달라질건 없었습니다.

File은 같은 value로 원하는 수만큼 추가하면 되고,

Dto는 똑같이 json 방식으로 작성해주고, List가 되었으니 대괄호 "[]" 만 추가하면 되겠습니다. (중괄호 "{}" 아닙니다!)

 

이제 Send를 누르면 데이터가 잘 들어가는 것을 확인할 수 있습니다.

 

확인해야할 부분은

Postman에서 입력할 KEY와 @RequestPart의 value로 지정한 값이 일치해야 한다는 것

같은 KEY로 파일을 등록하면 List가 된다는 것

Dto는 form-data에서 json 방식으로 등록해야 하며, List는 대괄호를 추가해야 한다는 것

Dto의 contentType을 application/json으로 맞춰야 한다는 것

정도가 되겠습니다.

 

이 글은 의존성 주입과 db연결을 다루지 않으며, 오로지 Entity 구현 만을 다룹니다.

 

userId를 기본 키로 갖는 User Entity입니다.

나중에 작성할 UserTitle Entity와 1:N 관계를 나타내기 위해 @OneToMany 어노테이션을 사용하였습니다.

 

다음은 TitleId를 복합 키로 갖는 Title Entity입니다.

@IdClass로 구현하였으며, 관련 구현 방법은 [여기]에 있습니다.

사실 복합 키로 구현하지 말고, titleId를 기본 키로 두는게 좋지만 학습을 위해 titleName까지 묶어서 복합 키를 구현하였습니다.

 

이제 저는 User와 Title Entity의 N:N 관계를 UserTitle Entity로 구현할 생각이며, 각각 Entity의 key들을 외래 키이자 기본 키로 사용하려고 합니다.

우선 UserTitle Entity와 1:N 관계를 위해 @OneToMany 어노테이션을 추가합니다.

mappedBy를 확실히 지정해줍니다!

 

이것 또한 IdClass로 구현하기 위해 UserTitleId를 구현하였습니다.

IdClass의 변수로는 참조할 key를 적어주시면 되는데

User는 String인 userId

Title은 Entity 자체인 Title을 자료형으로 넣어줍니다.

변수 명은 아까 User와 Title에서 mappedBy로 작성해둔 이름으로 맞춰줍니다.

UserTitle 최종본입니다.

클래스 위에 @IdClass 어노테이션 달아주시고,

참조할 Entity를 자료형으로 가지는 변수를 생성해주시면 됩니다.

여기서 변수명은 mappedBy와 통일시켜야 합니다!

그리고 JoinColumn을 이용해 참조하는 필드 이름(referencedColumnName)과 데이터베이스에 표시될 이름(name)을 지정해줍니다.

 

이제 실행합니다!

로그를 보시면 테이블이 잘 생성되었고, Foreign Key도 설정이 잘 되었음을 확인할 수 있습니다.

그리고 이렇게 테이블이 잘 생성되었음을 알 수 있습니다!

 

다음에는 Embeddable으로 구현해서 포스팅해볼 생각입니다.

보통 Controller에서 Dto를 받을 때는 @RequestBody를 주로 사용합니다.

그리고 File을 받을 때는 MultipartFile 객체를 사용하며, @RequestParam을 사용합니다.

 

하지만 File과 Dto를 같이 받기 위해서는 @RequestPart라는 어노테이션이 필요합니다.

File은 @RequestParam, Dto는 @RequestBody를 사용해서 받을려고 했지만 몇번을 시도해도 안되더라고요..

그래서 구글링을 통해 RequestPart라는 어노테이션을 찾아냈습니다.

 

우선 Controller입니다.

File과 Dto 모두 @RequestPart 어노테이션으로 받도록 합니다.

 

그리고 Dto인 UserInsertPostReq는 이렇게 구성되어 있습니다.

이전글 에서 사용했던 Dto와 동일합니다.

 

그리고 Controller에서 호출할 Service의 insertUser 메소드입니다.

파일을 저장할 경로를 지정한 후에 지정한 경로에 파일을 저장하고 DB에 유저 정보를 추가하는 로직입니다.

 

저는 이를 수행하기 위해 포스트맨을 사용하였습니다.

Key name은 @RequestPart 에서 value로 지정해준 name과 동일한 Key name으로 넣어줍니다.

이제 저기있는 Send를 눌러볼까요?

 

네 역시나 400에러가 발생합니다.

 

구글링을 통해 value에 json형식으로 넣는 방법을 알게되었고, 그대로 따라해보았습니다.

이런식으로 Key name은 userInsertPostReq, value는 json 방식으로 작성하였습니다.

다시 Send를 눌러보겠습니다.

 

이번에는 415 에러가 발생합니다..

 

한 두 시간 동안은 400, 415에러만 봤던것 같습니다.

 

이 에러들을 해결하기 위해 구글링을 해본 결과로는

여기서 Dto인 userInsertPostReq는 content type을 application/json으로 지정하면 해결할 수 있습니다.

userInsertPostReq의 content type을 application/json으로 지정한 후 Send를 누릅니다.

 

그럼 입력한 정보가 잘 들어가는 것을 확인할 수 있습니다.

 

File과 Dto를 같이 받아올 때 Key name과 content type 설정을 잘못하여 나온 에러 상황이었습니다.

이 방법 외에 다른 방법이 있는지는 잘 모르겠습니다. 더 많은 공부가 필요할 것 같습니다.

 

지금까지 단일 File과 Dto를 요청하는 것에 대해 알아보았고, 다수의 File과 Dto 요청에 관해서는 아래 링크에서 확인하시면 됩니다.

https://emoney96.tistory.com/375

 

Postman을 이용한 다수의 File, Dto 동시 Post 요청

https://emoney96.tistory.com/258 Postman을 이용한 MultipartFile, Dto 동시 Post요청 보통 Controller에서 Dto를 받을 때는 @RequestBody를 주로 사용합니다. 그리고 File을 받을 때는 MultipartFile 객체를 사..

emoney96.tistory.com

 

프로젝트 진행 중 JpaRepository에서 Pagenation을 적용한 Page객체를 받은 이후 Collections.sort로 정렬을 하였습니다.

하지만 이와 관련된 문제점 하나를 발견하게 되었고 포스팅으로 남길려고 합니다.

 

우선 db에 유저의 정보를 저장해줍니다.

profile_url은 file저장을 공부하면서 사용해본 것이며 이 글과는 상관 없는 file입니다.

내일쯤 file 저장관련 글을 포스팅할 예정입니다.

 

각 유저들의 점수(score)를 같이 저장해줍니다.

이 유저들을 score기준 내림차순으로 조회를 하는데 Pagenation을 적용시키려고 합니다.

 

 

UserRepository에서는 Page 객체로 리턴하는 findAll 메소드를 작성합니다.

 

UserServiceImpl에서 UserRepository의 findAll 메소드를 호출하는 findAllUser 메소드를 작성합니다.

userRepository.findAll로 Page객체를 가져오고, UserGetRes의 of 메소드에서 정렬을 진행합니다.

 

Pagenation을 적용했기 때문에 변수를 많이 사용하였지만 of 메소드의 정렬하는 부분이 핵심입니다.

Page객체를 가져와서 score기준 내림차순으로 정렬을 진행합니다.

 

그리고 Controller에서는 page와 size를 RequestParam으로 받아서 UserService의 findAllUser 메소드를 호출합니다.

 

이제 스웨거를 통해 테스트를 합니다.

확인할 페이지는 0페이지, 즉 첫번째 페이지를 조회하며 최대 3개의 데이터를 조회하려고 합니다.

 

결과는...

네.. 이렇게 나왔습니다.

 

원래 의도한 대로라면 score가 120인 id10부터 score가 100인 id2까지 나오는게 맞습니다.

여기서 확인해봐도 id는 10 -> 5 -> 2 순서대로 조회가 되어야합니다.

 

이 과정에서 제가 치명적인 실수를 한 것이 있습니다.

바로 Repository에서 Page객체를 가져온 이후에 정렬을 진행했다는 것입니다.

 

Page객체에 있는 데이터는 조회 가능한 모든 리스트가 있는것이 아니라 size만큼의 데이터만 있는 것이었습니다.

따라서 제가 Service에서 Repository를 호출할 때 Sort 기준을 아무것도 주지 않았기 때문에 먼저 추가된 데이터부터 size만큼 데이터가 조회된 것이었습니다.

 

그럼 이제 알맞게 수정을 해야겠죠?

우선 UserRepository의 findAll 메소드에 OrderByScore를 추가하거나 Pageable에 Sort를 추가합니다.

그 후에 UserGetRes의 정렬하는 부분을 지웁니다.

 

일단 UserGetRes의 of 메소드의 정렬부분을 지웁니다.

 

 

먼저 UserRepository의 findAll 메소드 수정하는 방법입니다.

findAllUsers로는 자동완성이 안뜨길래 findUsersByOrderByScoreDesc로 수정합니다.

뒤에 Desc를 안붙이면 default로 Asc가 적용됩니다.

 

 

다음은 UserService의 PageRequest에 Sort를 추가하는 방법입니다.

UserRepository에 있는 findAll 메소드는 그대로 두고 Sort를 추가합니다.

score 기준 DESC를 추가합니다.

 

 

둘 중 아무방법이나 사용한 다음 스웨거를 통해 테스트를 합니다.

그러면 이렇게 올바르게 조회를 할 수가 있습니다.

 

저는 두 번째 방법을 선호하기는 합니다만 두 방법 중 어떤 방법이 더 좋을지는 잘 모르겠습니다.

앞으로 로직을 구현할 때 생각이라는 것을 많이 해야겠다는 생각이 듭니다.

 

정렬을 적용하는 시점이 잘못되어 생긴 문제였습니다.

더미데이터를 작성하던 도중 겪었던 문제를 포스팅 하려고합니다.

 

우선 save는 JpaRepository에 내장되어있는 메소드입니다.

save가 수행하는 작업은 insert와 update 이렇게 두가지입니다.

insert와 update로 나뉘는 기준은 select를 했을 때 PK가 있으면 update, 없으면 insert입니다. (물론 null이어도 insert입니다.)

 

우선 User Entity를 작성합니다.

PK는 id이고 auto_increment 속성을 추가하였습니다.

 

그리고 UserRepository를 생성만 해줍니다.

어차피 JpaRepository 내장메소드인 save를 사용하므로 내용은 없습니다.

 

그리고 UserRepository에 대한 Test를 작성해주고 실행합니다.

 

어? save를 두번했으니 2L아닌가요??

심지어는 두번째로 save한 데이터로 덮어져 있습니다.

 

sql문 실행 로그를 확인해보았더니 insert를 했다가 update를 합니다.

 

사실 확인을 위해 save이후의 id를 확인해보았습니다.

 

여기서 insert 직후 id가 1로 출력이 되었습니다.

save(user)를 하였는데 user에 자동으로 insert된 row가 저장되는 모양입니다.

 

이를 해결하기 위해서는 두 번째 save를 하기 전에 id를 null로 바꿔주어야합니다.

user.setId(null);을 추가합니다.

 

이제 insert가 두 번 되었음을 확인할 수 있습니다.

 

db에도 잘 들어갔네요!

 

save를 하면 Entity 객체에 자동으로 값이 들어간다는 사실을 몰라서 겪었던 상황이었습니다.

+ Recent posts