back-end/프로젝트

[매장 예약]

림가이드 2023. 5. 25. 19:01

< 서비스 >

  • 매장을 방문할때 미리 방문 예약을 진행하는 기능"을 구현
  • 위 예약을 받기 위해서는 점장(매장의 관리자)이 매장정보를 등록하고자 하는 기능을 구현
  • 매장을 등록하기 위해서는 파트너 가입 기능"이 구현
  • 매장 이용자가 서비스를 통해서 매장을 검색하고 상세 정보를 확인하는 기능을 구현
  • 서비스를 통해서 예약한 이후에, “예약 10분전에 도착하여 키오스크를 통해서 방문 확인을 진행하는
  • 기능"을 구현
  • 예약 및 사용 이후에 리뷰를 작성하는 기능을 구현

 

< 시나리오 >

  • 매장의 점장은 예약 서비스 앱에 상점을 등록매장 명, 상점위치, 상점 설명)
  • 매장을 등록하기 위해서는 파트너 회원 가입(따로, 승인 조건은 없으며 가입 후 바로 이용 가능)
  • 매장 이용자는 앱을 통해서 매장을 검색하고 상세 정보를 확인
  • 매장의 상세 정보를 보고, 예약을 진행 (예약을 진행하기 위해서는 회원 가입이 필수적으로 이루어 져야함)
  • 서비스를 통해서 예약한 이후에, 예약 10분전에 도착하여 키오스크를 통해서 방문 확인을 진행
  • 예약 및 사용 이후에 리뷰를 작성 가능
개발 흐름도

< 구현 >

- 회원 가입 (점장 및 사용자)

- 매장 등록 (점장)

- 예약 진행 (예약 가능 여부 확인 및 예약)

- 도착 확인

- 리뷰 작성

 


< 프로젝트 세팅 >

1. 개발 환경

- JDK 11 이상

- SpringBoot

- Gradle-Java

- Docker

- MySQL(DB)

- Redis

- Swagger(Test UI)

- Login Token (JWT)

 

2. 모듈 구성

- user-api

- reservation-api

- store-api

 

 

>> 참고 build.gradle

1. mysql

: MySQL db와 연결을 위한 JDBC 드라이버

implementation 'mysql:mysql-connector-java'

 

2. JPA

: 자바에서 관계형 DB와 상호작용하기 위한 API

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

- 데이터베이스 연결, 트랜잭션 관리, 쿼리 실행 등을 자동으로 구성

- 객체-관계 매핑 수행하고, DB와의 CRUD 작업을 간편하게 처리

= Entity class의 스캔, 트랜잭션 관리, JPA 기반 repository 작성

 

JDBC(Java Database Connectivity), JPA의 차이점?!

: jdbc은 자바 어플리케이션과 DB의 연결해줌. 직접 SQL query를 실행하고 db와 상호작용.

  즉, connection 객체가 db와 app의 연결을 관리하고, preparedstatement가 sql을 전달하며, resultset 객체를 통해 결과값을 전달.

  jpa는 객체와 db 간의 매핑 작업을 자동으로 하고, JPA repository를 통해 db에 접근 가능.

(직접 SQL문을 작성하지 않아도 됨)

 

 

3. Swagger

implementation 'io.springfox:springfox-boot-starter:3.0.0'

 

4. springCloud

: app을 작은 조각으로 나눠 개발하는 방식을 지원하는 도구/라이브러리 모음

ext{
    set('springCloudVersion',"2021.0.1")
}

dependencyManagement{
    imports{
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}

- dependencyManagement

: 프로젝트의 의존성 관리를 중앙 집중화하고, 모듈 간의 의존성 충돌 방지

 

등등...

 

 

3. db 설정

1) mysql

- 사용자 생성 및 권한 부여

> create database users;
> create database store;

> use mysql;
> create user 'reservation'@'%' identified by 'reservation';
> grant all privileges on *.* to 'reservation'@'localhost';

> flush privileges;

- docker

% docker run -d \
--name rv-mysql \
-e MYSQL_ROOT_PASSWORD="rootpw123" \
-e MYSQL_USER="reservation" \
-e MYSQL_PASSWORD="reservation" \
-e MYSQL_DATABASE="reservation" \
-p 3306:3306 \
-d mysql

- mysql connection in IntelliJ

 

 

 

 

 

 


< Annotation >

 

 

- @EntityListeners(value = {AuditingEntityListener.class})

: @EntityListeners : Entity Class에 적용되고, 해당 entity의 생명 주기 이벤트를 감지할 리스너 클래스를 지정

: AuditingEntityListener.class :  자동으로 생성일자/수정일자를 관리하여 기록

- @EnableJpaAuditing

: 메인 Application에서 사용. entity의 생성일자/수정일자를 자동 관리

 

- hibernate envers

: db entity의 변경이력을 추적하고 관리 기능

- @AuditOverride(forclass = )

: envers를 통해 변경 이력을 추적하는 동안 특정 속성의 값을 재정의/구성

> for use: implementation 'org.springframework.data:spring-data-envers'

 

 

- @EnableJpaRepositories

: 메인 Application에서 사용. repository interface를 사용하기 위함.

- @ServletComponentScan

: 메인 Application에서 사용. 서브렛(client 요청을 처리하고 응답하는 것) 기반 컴포넌트를 자동으로 스캔하고 등록.

즉, web application server에서 동작하고 HTTP 프로토콜을 기반으로 클라이언트와 통신하는데 필요한 컴포넌트들을 자동으로 등록해주는 것.

- @EnableFeignClients

: Feign(web application에서는 다른 서비스나 API와 통신해야하는데 이를 쉽게 처리) 클라이언트를 활성화

 

- @EnableRedisRepositories

: Redis 리포지토리 활성화. Redis를 사용하여 데이터에 대한 저장소 및 조회 기능을 제공하는 인터페이스 정의

 

- @EntityGraph

: entity와 관련된 데이터를 로딩할 때 db에서 필요한 연관 관계를 함께 로딩하는 기능 제공

엥? 이게 무슨 말?!

ex)  ProductRepository.interface

@EntityGraph(attributePaths = {"productItemList"}, type = EntityGraph.EntityGraphType.LOAD)
Optional<Product> findWithProductItemById(Long id);

   attributePaths: Product엔티티의 productItemList 속성에 대한 로딩시, 연관관계 지정

   type: EntityGraphType 열겨형을 사용하여 연관관계를 즉시 로딩

=> 이 메서드를 호출하면 Product 엔티티와 관련된 productItemList 연관관계를 함께 로딩하여 반환

 

 

 


< JWT > :  Base64로 인코딩된 문자열로 이루짐

 

1. 라이브러리 추가

implementation 'io.jsonwebtoken:jjwt:0.9.1'

 

2. 토큰 생성

1) claims : JWT에서 사용하는 정보 조각

- {키: 값, ... }으로 구성

- 사용자의 고유식별자, 권한 정보 등을 토큰의 클레임에 포함

 

ex) subject: 토큰이 대상으로 하는 사용자를 식별하는 용도, id: 토큰의 고유 식별자, roles: 개발자가 직접 설정

Claims claims = Jwts.claims().setSubject(Aes256Util.encrypt(userPk)).setId(Aes256Util.encrypt(id.toString()));
claims.put("roles", userType);

2) 토큰 생성

// Jwt(토큰) 생성하여 문자열로 반환
return Jwts.builder()
        .setClaims(claims)
        .setIssuedAt(now)
        .setExpiration(new Date(now.getTime() + tokenValidTime))
        .signWith(SignatureAlgorithm.HS256, secretKey)
        .compact();

- signWith(....)

: 이 토큰이 변조되지 않았는지 확인해야 하는데, 이때 디지털 서명(토큰이 변조되지 않았음을 검증하는 역할)을 사용한다.

디지털 서명은 토큰의 내용과 함께 서명에 사용되는 비밀키(private key: 토큰을 생성할 때 사용하는 비밀값으로 client에게는 공개되지 않음)를 기반으로 생성한다.

서명에 사용되는 알고리즘은 서명 생성 및 검증에 사용되는 알고리즘

 

3. 토큰 유효 확인 (로그인 시)

1) Jwts: JWT를 생성하고 해석

- 구성: header, claim, signature

즉, Jws 객체를 생성

2) Jws: JWT의 signature 부분

- 서명 알고리즘과 실제 서명값 포함

즉, Jwts를 통해 생성된 서명 부분으로 이를 검증하는데 사용

Jws<Claims> : JWT의 서명 객체인 Jws에 포함된 claim 정보를 가짐

 

위를 이해하였으면, 아래가 이해갈 것임

Jws<Claims> claimsJws = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
return !claimsJws.getBody().getExpiration().before(new Date());

 

 

 

위를 사용하려면 bean 등록(해당 빈을 다른 컴포넌트에서 주입하여 사용할 수 있도록 함)

@Configuration
public class JwtConfig {
    @Bean
    public JwtProvider jwtAuthenticationProvider() {
        return new JwtProvider();
    }
}

 


< Query DSL>

: DB query를 프로그래밍 방식으로 작성할 수 있게 도와주는 라이브러리

 

ex) 가나다순 정렬, 별점순 정렬 등

 

1. build.gradle dependencies에 추가

    // queryDSL
    implementation 'com.querydsl:querydsl-jpa:5.0.0'
    annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jpa'
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
    annotationProcessor 'jakarta.annotation:jakarta.annotation-api'

build시, model 내에 있는 파일들이 'Q{모델들}'로 해당모듈 내에 존재할 것임!

 

2. Configuration

@Configuration
public class QueryDslConfig {
    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}

 

3. CustomRepository

public interface StoreRepositoryCustom {
    List<Store> searchByName(String name);
}

 

@Repository
@RequiredArgsConstructor
public class StoreRepositoryImpl implements StoreRepositoryCustom {
    private final JPAQueryFactory queryFactory;

    @Override
    public List<Store> searchByName(String name) {
        String search = "%" + name + "%";

        QStore qStore = QStore.store;
        return queryFactory.selectFrom(qStore)
                .where(qStore.name.like(search))
                .fetch();
    }
}
@Repository
public interface StoreRepository extends JpaRepository<Store, Long>, StoreRepositoryCustom{

}

 


< 멀티 모듈 >

ex) 만약, reservation-api에서 store-api에 접근하고 싶다면?

 

 

1. build.gradle에 의존성 추가

implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'

 

2. UserClient 생성

@FeignClient(name = "store-api", url = "${feign.client.url.store-api}")
public interface UserClient {

    // 예약 가능여부
    @GetMapping("/store/detail")
    ResponseEntity<StoreDto> getDetail(@RequestParam Long storeId);
}

 

3. 사용

@Service
@RequiredArgsConstructor
public class CustomerReservationService {
    private final ReservationRepository reservationRepository;
    private final UserClient userClient;

    // 고객 - 예약
    public Reservation isPossible(String userPhone, AddReserveForm reserveForm) {
        StoreDto storeDto = userClient.getDetail(reserveForm.getStoreId()).getBody();
        if (!storeDto.isPossibleUse()) {
            throw new CustomException(ErrorCode.CANNOT_RESERVE);
        }

        return reservationRepository.save(Reservation.from(reserveForm, userPhone));
    }
}

 

직접적으로 Repository에 접근하는 방식이 있는지는 조금 더 공부해야 하겠지만,

위의 방식은 UserClient로 store 모듈의 controller에 접근할 수 있도록 하는 코드이다.