[프로젝트] 우리들의 여행계획을 책임지는 Triplan🛫

👻taz.dev🖤·2022년 3월 26일
post-thumbnail

안녕하세요! Triplan의 BackEnd 개발자 박정현입니다!

저희 팀은 여행 계획을 세우는데 어려움을 겪는 사람들을 위해 쉽고 빠르게 여행 계획을 세울 수 있도록 도와주는 앱을 만들기 위해 "여행플래너" 라는 주제로 팀프로젝트를 진행하였습니다.

😎 Github : https://github.com/taz-dev/triplan

🔸 프로젝트 소개

  • 지도에서 위치등록을 통해 한눈에 동선을 파악할 수 있어요!
  • 체크리스트를 통해 여행에 필요한 물품들을 꼼꼼하게 확인할 수 있어요!
  • 여행에 소비한 경비와 간단한 메모작성을 통해 여행 경비 정산을 쉽게 할 수 있어요!
  • 친구와 함께 여행일정 공유를 할 수 있어요!

🔸 프로젝트 기간 & 팀원 구성

  • 기간 : 2021.11 ~ 2021.03 (약 4개월)
  • PM : 김환
  • UI/UX : 김종표
  • FrontEnd : 최은석
  • BackEnd : 박정현

🔸 기술 스택

  • FrontEnd : React Native, Node.js
  • BackEnd : Java(11), SpringBoot(2.6), Spring Security, Spring Data JPA, MariaDB

🔸 Triplan DB 구성도

프로젝트를 진행하면서 JPA 기술을 학습하고 싶어서 엔티티들 간의 다양한 연관관계 매핑을 통한 도메인 테이블을 설계하였습니다. 처음에 시간이 걸리더라도 DB 설계를 확실하게 해놔야 개발할 때 편하기 때문에 다양한 경우를 생각해보면서 DB 설계를 했습니다. 나중에 개발하면서 여러번 수정하긴 했지만..

어려웠던 부분은 친구 초대 기능을 만들고 싶으면 엔티티들 간의 어떤 연관관계를 맺어야 하는지에 관한 부분이였습니다.

MEMBER 테이블과 PLAN 테이블이 있을 때

1. 멤버는 여러 개의 플랜을 가질 수 있다.
2. 플랜도 여러 명의 멤버를 가질 수 있다. (친구와 공유를 하게 되었을 때)

그럼 @ManyToMany 를 써야 하잖아?
하지만 실무에서는 @ManyToMany 를 사용하면 안된다!

  1. 매핑 정보만 넣는 것이 가능하고, 추가 정보를 넣는 것 자체가 불가능하다.
  2. 중간 테이블이 숨겨져 있으므로 예상하지 못한 쿼리가 나간다.

그래서 PLANJOIN 이라는 조인 테이블을 만들어서 MEMBERPLAN 의 다대다 관계를 일대다, 다대일 관계로 풀어서 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<>();
    ...
}

🔸 주요 기능

1. OAuth2.0을 이용한 Kakao 회원가입 및 로그인

웹/앱 서비스를 이용하다 보면 Google, Naver, Kakao 등 외부 소셜 계정을 기반으로 간편하게 회원가입 및 로그인을 할 수 있는 어플리케이션을 쉽게 찾아볼 수 있습니다.

클릭 한번으로 간편하게 로그인할 수 있을 뿐만 아니라, 연동되는 외부 어플리케이션에서 Google, Naver, Kakao 등에서 제공하는 기능을 간편하게 사용할 수 있기 때문에 새로운 프로젝트를 진행하게 되면 꼭 OAuth 소셜 로그인 기능을 넣어보고 싶어서 이번 프로젝트에 넣게 되었습니다.

저희는 따로 로그인 서비스를 만들지 않고 Kakao로부터 유저정보를 받아 회원가입 후 로그인을 진행할 수 있도록 하였습니다.

[1-1] Kakao 로그인 프로세스

  1. 서비스 접근 및 이용 시도
  2. 접근 성공 시 Kakao(Resource Server)에서 code 발급
  3. code로 Kakao에게 access token 요청
  4. Kakao에서 access token 반환
  5. 받아온 access token을 Triplan Server(Authorization Server)로 전달
  6. access token을 Kakao로 전달하여 유저 정보 요청
  7. 유저 정보 반환(email, nickname, imageUrl)
  8. 유저 정보 Triplan DB에 저장(회원가입)
  9. 유저 인증 후 JWT 인증 토큰 발급

2. JWT(Access Token, Refresh Token)로 보안성 강화 및 사용자 편의성 고도화

카카오 로그인은 완료가 되지만, 여기서 추가적으로 저의 서비스 토큰으로 변환하는 작업을 추가하였습니다.

@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 를 이용하여 로그인이 완료된 후 간단 설정을 통해 닉네임과 자기소개 입력을 마치면 이제 저희 서비스를 이용하실 수 있어요!


3. RestAPI에 대한 이해를 바탕으로 API 설계(명사를 통한 리소스 식별, 헤더에 데이터 포맷 포함)


4. 여행비용, 위치설정, 체크리스트를 통해 빠르고 간편한 여행일정 짜기

[4-1] 일정 저장(시간, 여행비용, 메모)

[4-2] 체크리스트


5. 지도에 핀을 지정하여 여행경로 한눈에 쉽게 파악하기

[5-1] 핀을 이용한 경로 파악


6. 내 프로필 변경하기(닉네임, 자기소개)

[6-1] 닉네임, 자기소개 수정


7. 문의하기

[7-1] 문의하기


8. 친구 초대 기능을 이용하여 나의 계획 공유하기

[8-1] 친구 초대

9. 배포


🔸 느낀점

0개의 댓글