본문 바로가기

포트폴리오/주차장 서비스(with turu parking)

[포트폴리오/주차장 서비스(with turu parking)] - (2) 백엔드 개발

Github

https://github.com/mii2026/sw15

 

GitHub - mii2026/sw15

Contribute to mii2026/sw15 development by creating an account on GitHub.

github.com

 

 

목차
1. 들어가며
2. 패키지 분리
3. 로그인 관련 기능 개발
4. 마치며

 

 

 

들어가며

 이번 글은  주차장 서비스의 백엔드 서버 개발에 관하여 작성하려고 한다. 코드에 관해서는 설명이 필요한 부분 위주로만 간단하게 정리하였다. 개발에는 Spring Boot를 사용하였고 IDE로는 Intellij를 사용하였다.

 

 

 

 

 

패키지 분리

더보기

 

클래스 역할별로 패키지 분리

 

 

 

 

 

 백엔드 서버의 클래스들은 위와 같이 패키지 별로 정리하였다. Config, Controller, Dto, Entity, Service등 스프링을 사용한 백엔드 개발 시 필수적으로 사용되는 패키지들을 제외하고 나머지는 jwt 로그인 관련, 예외 처리 관련 패키지들을 선언하여 정리하였다.

 

 

 

 

 

 앞서 언급하였듯 로그인에는 카카오, 구글, 네이버와 같은 소셜 로그인이 아니라 jwt를 사용하였다. 이 부분에서 사용자 session 관리를 위해 redis를 연동하여 사용하였고 이에 대한 자세한 이야기는 다른 글에서 정리할 예정이다.

 

- PayController.java

더보기
/*
 * 결제 수단 확인
 */
@GetMapping
public ResponseEntity<List<PayDto>> checkPaymentInfo(@RequestHeader("Authorization") String requestAccessToken) throws Exception {

    String userToken = null;

    if (requestAccessToken != null && requestAccessToken.startsWith("Bearer ")) {
        userToken = requestAccessToken.substring(7, requestAccessToken.length());
    }

    if(userToken == null || !jwtTokenProvider.validateToken(userToken)) {
        throw new ApiException(ErrorCode.BAD_REQUEST, "유효하지 않은 토큰입니다.");
    }

    String userName = jwtTokenProvider.getUsername(userToken);
    Optional<User> user = userRepository.findById(userName);

    if(user.isEmpty()) {
        throw new ApiException(ErrorCode.NULL_POINT, "잘못된 유저 정보입니다.");
    }

    List<PayDto> payDtos = payService.getPayByUserId(user.get().getUserId());

    return new ResponseEntity<List<PayDto>>(payDtos,
            HttpStatus.OK);
}

 

 

 

 

 내가 작성하였던 컨트롤러 코드의 일부이다. 사용자의 정보를 HTTP 메시지에서 긁어와서 연결된 DB에 Select 쿼리를 날려서 사용자가 가진 결제 수단들의 정보를 리스트 형태로 보내주는 기능을 하는 API이다.

 

 

 

 

 만약 HTTP 메시지에 유효한 토큰값이 들어있지 않은 경우 서버에서는 예외처리를 시키도록 하였다. 만약 해당 토큰이 유효한 값이라면 토큰을 통해 유저정보를 추출하여 DB로 쿼리문을 날려서 답변을 받아온다.

 

 

 

 

 

 

로그인 관련 기능 개발

- PayServiceImpl.java

더보기
@Service
public class PayServiceImpl implements PayService {

    private final PayRepository payRepository;
    private final UserRepository userRepository;

    @Autowired
    public PayServiceImpl(PayRepository payRepository, UserRepository userRepository) {
        this.payRepository = payRepository;
        this.userRepository = userRepository;
    }

    @Override
    public PayDto getPayById(Long payId) throws EntityNotFoundException {

        Pay payEntity = payRepository.getReferenceById(payId);

        return PayDto.of(payEntity);
    }

    @Override
    public List<PayDto> getPayByUserId(Long userId) {

        List<Pay> payEntity = payRepository.findByUserUserId(userId);
        List<PayDto> payDtos = new ArrayList<>();
        for (Pay pay : payEntity) {
            payDtos.add(PayDto.of(pay));
        }

        return payDtos;
    }

    @Override
    @Transactional
    public void updatePayInfo(PayDto payDto) throws EntityNotFoundException {

        Pay pay = payRepository
                .findById(payDto.getPayId())
                .orElseThrow(() -> new ApiException(ErrorCode.NULL_POINT, "결제수단 정보가 없습니다."));

        pay.setPayName(payDto.getPayName());
        pay.setPayType(payDto.getPayType());
        pay.setPayNumber(payDto.getPayNumber());

        payRepository.save(pay);
    }


    @Override
    @Transactional
    public void registerPayInfo(PayDto payDto, Long userId) throws EntityNotFoundException {

        Pay pay = Pay.builder()
                .payName(payDto.getPayName())
                .payType(payDto.getPayType())
                .payNumber(payDto.getPayNumber())
                .build();

        User user = userRepository
                .findById(userId)
                .orElseThrow(() -> new ApiException(ErrorCode.NULL_POINT, "존재하지 않는 사용자입니다."));

        pay.setUser(user);

        if(pay.getPayType().equals("현금") || pay.getPayType().equals("기타")) {
            pay.setPayName(" ");
            pay.setPayNumber(0);
        }

        payRepository.save(pay);
    }

    @Override
    @Transactional
    public PayDto deletePayInfo(Long payId) throws EntityNotFoundException {

        Pay pay = payRepository
                .findById(payId)
                .orElseThrow(() -> new ApiException(ErrorCode.NULL_POINT, "결제수단 정보가 없습니다."));

        payRepository.deleteById(payId);

        return PayDto.of(pay);
    }
}

 

 

 

 

 Controller에서 넘어온 데이터들을 처리하는 로직들을 모아놓은 PayService 코드이다. 사용자 결제수단 정보를 획득하여 반환하거나 결제수단 등록, 수정, 삭제가 실제로 이루어지는 로직이다.

 

 

 

 

 JPA를 이용하여 DB와 연동하였기 때문에 코드내에서 실제로 쿼리문을 생성하여 날리는 부분은 존재하지 않는다. 토큰 유효성 검증은 컨트롤러 코드에서 진행하는 것으로 약속하였기 때문에 서비스 코드에는 DB 통신과 관련된 로직들만 작성하였다.

- JwtTokenProvider.java

더보기
@Component
public class JwtTokenProvider {

    @Value("${jwt.secret}")
    private String secretKey;

    @Value("${jwt.expiration}")
    private long validityInMilliseconds;

    private final UserDetailsService userDetailsService;

    public JwtTokenProvider(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    public String createToken(String username) {
        Claims claims = Jwts.claims().setSubject(username);
        Date now = new Date();
        Date validity = new Date(now.getTime() + validityInMilliseconds);

        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(validity)
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }

    // 토큰에서 인증 정보 추출
    public Authentication getAuthentication(String token) {
        UserDetails userDetails = this.userDetailsService.loadUserByUsername(getUsername(token));
        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }

    // 토큰에서 사용자 이름 추출
    public String getUsername(String token) {
        return Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody().getSubject();
    }
    // HTTP 요청에서 JWT 토큰 해결
    public String resolveToken(HttpServletRequest req) {
        String bearerToken = req.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7).trim();
        }
        return null;
    }

    // JWT 토큰 유효성 검사
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    public long getRemainingSeconds(String token) {
        try {
            DefaultClaims claims = (DefaultClaims) Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody();
            return (claims.getExpiration().getTime() - Instant.now().toEpochMilli()) / 1000;
        } catch (JwtException | IllegalArgumentException e) {
            throw new InvalidJwtAuthenticationException("만료됐거나 유효하지 않은 토큰입니다.");
        }
    }
}

 

 

 

 

 jwt 발급, 검증을 수행하는 클래스이다. 컨트롤러단에서 수행하는 토큰 검증 및 로그인 시 발급되는 토큰은 모두 이 클래스를 통해서 이루진다.

 

 

 

 

 이 중에서 가장 많이 사용하였던 메소드는 토큰 값에서 유저 이름을 추출하는 getUsername 이었다고 생각한다. DB접근시 table에서 기본적으로 Username이 필요한 table이 많았기 때문에 대부분의 API에서 사용하였다.

 

 

 

 

 

 

 

 

 

마치며

 이번 글에서는 백엔드 서버의 구조를 정리하고, 로그인 관련 기능을 구현하여 간단하게 테스트하는 내용까지를 정리하여 보았다. 앞서 언급하였듯이 백엔드 서버 코드 중 설명이 필요한 부분 중 일부분만을 정리하였다. 자세한 코드는 깃헙 링크를 통해 확인 가능하다.