
안녕하세요! Triplan의 BackEnd 개발자 박정현입니다!
저희 팀은 여행 계획을 세우는데 어려움을 겪는 사람들을 위해 쉽고 빠르게 여행 계획을 세울 수 있도록 도와주는 앱을 만들기 위해 "여행플래너" 라는 주제로 팀프로젝트를 진행하였습니다.
😎 Github : https://github.com/taz-dev/triplan
프로젝트를 진행하면서 JPA 기술을 학습하고 싶어서 엔티티들 간의 다양한 연관관계 매핑을 통한 도메인 테이블을 설계하였습니다. 처음에 시간이 걸리더라도 DB 설계를 확실하게 해놔야 개발할 때 편하기 때문에 다양한 경우를 생각해보면서 DB 설계를 했습니다. 나중에 개발하면서 여러번 수정하긴 했지만..
어려웠던 부분은 친구 초대 기능을 만들고 싶으면 엔티티들 간의 어떤 연관관계를 맺어야 하는지에 관한 부분이였습니다.
MEMBER 테이블과 PLAN 테이블이 있을 때
1. 멤버는 여러 개의 플랜을 가질 수 있다.
2. 플랜도 여러 명의 멤버를 가질 수 있다. (친구와 공유를 하게 되었을 때)
그럼 @ManyToMany 를 써야 하잖아?
하지만 실무에서는 @ManyToMany 를 사용하면 안된다!
그래서 PLANJOIN 이라는 조인 테이블을 만들어서 MEMBER 와 PLAN 의 다대다 관계를 일대다, 다대일 관계로 풀어서 DB를 설계하게 되었습니다.
@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Member extends BaseTimeEntity implements UserDetails {
...
@OneToMany(mappedBy = "member", cascade = ALL)
private List<PlanJoin> planJoins = new ArrayList<>();
...
}
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PlanJoin {
@Id @GeneratedValue(strategy = IDENTITY)
@Column(name = "plan_join_id")
private Long id;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "member_id")
private Member member;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "plan_id")
private Plan plan;
}
@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Plan extends BaseTimeEntity {
...
@OneToMany(mappedBy = "plan", cascade = ALL)
private List<PlanJoin> planJoins = new ArrayList<>();
...
}
웹/앱 서비스를 이용하다 보면 Google, Naver, Kakao 등 외부 소셜 계정을 기반으로 간편하게 회원가입 및 로그인을 할 수 있는 어플리케이션을 쉽게 찾아볼 수 있습니다.
클릭 한번으로 간편하게 로그인할 수 있을 뿐만 아니라, 연동되는 외부 어플리케이션에서 Google, Naver, Kakao 등에서 제공하는 기능을 간편하게 사용할 수 있기 때문에 새로운 프로젝트를 진행하게 되면 꼭 OAuth 소셜 로그인 기능을 넣어보고 싶어서 이번 프로젝트에 넣게 되었습니다.
저희는 따로 로그인 서비스를 만들지 않고 Kakao로부터 유저정보를 받아 회원가입 후 로그인을 진행할 수 있도록 하였습니다.
[1-1] Kakao 로그인 프로세스
code 발급code로 Kakao에게 access token 요청access token 반환access token을 Triplan Server(Authorization Server)로 전달access token을 Kakao로 전달하여 유저 정보 요청JWT 인증 토큰 발급카카오 로그인은 완료가 되지만, 여기서 추가적으로 저의 서비스 토큰으로 변환하는 작업을 추가하였습니다.
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtProvider {
private final MemberRepository memberRepository;
@Value("jwt.secret")
private String secretKey;
private String ROLES = "roles";
private final Long accessTokenExpiry = 60 * 60 * 1000L; //1시간
private final Long refreshTokenExpiry = 14 * 24 * 60 * 60 * 1000L; //2주
private final UserDetailsService userDetailsService;
@PostConstruct
protected void init() {
secretKey = Base64UrlCodec.BASE64URL.encode(secretKey.getBytes(StandardCharsets.UTF_8));
}
// JWT 토큰 생성
public TokenDto createToken(Long memberId, List<String> roles) {
Member member = memberRepository.findById(memberId).get();
//Claims에 member 구분을 위한 Member pk 및 authorities 목록 삽입
Claims claims = Jwts.claims().setSubject(String.valueOf(memberId));
claims.put(ROLES, roles);
//생성날짜, 만료날짜를 위한 Date
Date now = new Date();
String accessToken = Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.setClaims(claims)
.setIssuedAt(now)
.signWith(SignatureAlgorithm.HS256, secretKey)
.setExpiration(new Date(now.getTime() + accessTokenExpiry))
.compact();
String refreshToken = Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.signWith(SignatureAlgorithm.HS256, secretKey)
.setExpiration(new Date(now.getTime() + refreshTokenExpiry))
.compact();
return TokenDto.builder()
.grantType("Bearer")
.accessToken(accessToken)
.refreshToken(refreshToken)
.accessTokenExpireDate(accessTokenExpiry)
.nickname(member.getNickname())
.email(member.getEmail())
.nameTag(member.getNameTag())
.aboutMe(member.getAboutMe())
.build();
}
}
[2-1] 간단 설정
이렇게 OAuth + JWT 를 이용하여 로그인이 완료된 후 간단 설정을 통해 닉네임과 자기소개 입력을 마치면 이제 저희 서비스를 이용하실 수 있어요!
[4-1] 일정 저장(시간, 여행비용, 메모)
[4-2] 체크리스트
[5-1] 핀을 이용한 경로 파악
[6-1] 닉네임, 자기소개 수정
[7-1] 문의하기
[8-1] 친구 초대
![]()
![]()