반응형

Spring Security를 이용해 회원 관리나 로그인 기능을 구현할 때, 비밀번호를 평문(plain text)으로 저장하면 보안상 매우 취약합니다. 이번 글에서는 Spring Security에서 제공하는 BCryptPasswordEncoder를 사용하여 안전하게 비밀번호를 암호화하는 방법을 소개하겠습니다.

 

 

1. BCryptPasswordEncoder란?

BCryptPasswordEncoder는 Spring Security에서 제공하는 비밀번호 암호화 도구로, 단방향 해싱(hashing)을 수행합니다. 한번 암호화된 비밀번호는 복호화가 불가능하며, 로그인 시에는 입력된 비밀번호를 다시 암호화하여 데이터베이스에 저장된 암호화된 비밀번호와 비교하여 인증합니다.

주요 특징:

  • 단방향 암호화 방식
  • 안전한 salt 자동 생성 및 관리
  • 비밀번호 강도 조절 가능

 

2. BCryptPasswordEncoder 설정 방법

다음은 Spring 프로젝트에서 BCryptPasswordEncoder를 Bean으로 등록하는 예제입니다.

@Configuration
public class PasswordConfig {

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

위 코드에서 @Configuration 어노테이션은 Spring에게 이 클래스가 설정 클래스임을 알려줍니다. 그리고 @Bean어노테이션을 사용하여 BCryptPasswordEncoder 인스턴스를 Spring의 관리 대상(Bean)으로 등록합니다.

 

3. 회원가입에서 사용 방법

회원가입 시 사용자의 비밀번호를 암호화하여 저장하려면 아래와 같이 작성합니다.

@Autowired
private BCryptPasswordEncoder passwordEncoder;

public void registerUser(User user) {
    String encodedPassword = passwordEncoder.encode(user.getPassword());
    user.setPassword(encodedPassword);
    userRepository.save(user);
}

 

4. 로그인 시 비밀번호 검증 방법

로그인 시 비밀번호를 검증할 때는 사용자가 입력한 비밀번호와 데이터베이스에 저장된 암호화된 비밀번호를 비교합니다.

@Autowired
private BCryptPasswordEncoder passwordEncoder;

public boolean authenticate(String rawPassword, String encodedPassword) {
    return passwordEncoder.matches(rawPassword, encodedPassword);
}

 

5. 주의사항

  • 절대 비밀번호를 평문으로 저장하지 말기!
  • strength 파라미터를 통해 암호화 강도를 조절할 수 있지만, 기본값(10)이 일반적인 환경에서 충분!
  • 성능이 매우 중요한 환경이라면 암호화 강도를 적절히 설정할 것!

마무리

지금까지 Spring Security의 BCryptPasswordEncoder를 활용한 안전한 비밀번호 관리 방법을 알아봤습니다. 이를 통해 애플리케이션 보안을 강화하고 사용자 정보를 안전하게 보호할 수 있습니다.

반응형

'Spring Boot > 스프링부트 예제' 카테고리의 다른 글

답변형 게시판 구현 (3)  (0) 2023.10.27
답변형 게시판 구현 (2)  (1) 2023.10.27
답변형 게시판 만들기 (1)  (1) 2023.10.26
QnA 다양한 검색 게시판 CRUD (1)  (1) 2023.10.24
게시판 페이징 처리  (1) 2023.10.23
반응형

Flutter의 initState 함수란?

Flutter에서 initState 함수StatefulWidget의 상태를 초기화할 때 사용하는 메서드입니다.
쉽게 말해, "앱이 처음 실행될 때 한 번만 호출되는 준비 작업 함수"라고 생각하면 됩니다.
예를 들어, 데이터를 불러오거나 타이머를 설정하는 등 초기화가 필요한 작업을 initState에서 수행합니다.


initState의 주요 특징

  1. 딱 한 번 호출됨
    • initState는 State 객체가 처음 생성될 때 한 번만 호출됩니다.
    • 이후 상태가 변경되어도 initState는 다시 호출되지 않습니다.
  2. 초기화 작업에 적합
    • 데이터를 불러오거나 리스너를 추가하는 등 초기 설정 작업에 사용됩니다.
  3. super.initState() 호출 필요
    • initState를 오버라이드할 때 super.initState() 를 반드시 호출해야 Flutter의 내부 초기화가 제대로 동작합니다.

initState 함수의 기본 구조

@override
void initState() {
  super.initState(); // 부모 클래스의 초기화 메서드 호출
  // 초기화 작업
}

간단한 예제: 앱 시작 시 타이머 설정

아래는 앱이 실행되면 3초 후에 텍스트를 업데이트하는 예제입니다.

import 'package:flutter/material.dart';
import 'dart:async'; // 타이머를 사용하기 위해 필요

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String message = "안녕하세요!";

  @override
  void initState() {
    super.initState();
    // 3초 후에 메시지 변경
    Timer(Duration(seconds: 3), () {
      setState(() {
        message = "Flutter를 배우고 있어요!";
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('initState 함수 예제')),
        body: Center(
          child: Text(
            message,
            style: TextStyle(fontSize: 24),
          ),
        ),
      ),
    );
  }
}

 


언제 initState를 사용해야 할까?

  • 초기 데이터 로드:
    • 예: API 호출, 데이터베이스에서 데이터 가져오기.
  • 애니메이션 시작:
    • 애니메이션 컨트롤러 초기화.
  • 리스너 추가:
    • 텍스트 입력이나 네트워크 이벤트를 감지하는 리스너 설정.
  • 타이머 설정:
    • 특정 시간 후에 동작을 실행.

 

주의할 점

  1. UI 관련 작업 금지:
    • initState에서 직접 UI를 업데이트하거나 context를 사용해 위젯 트리에 접근하면 오류가 발생할 수 있습니다.
    • 이유: 위젯이 아직 완전히 렌더링되지 않았기 때문입니다.
  2. 반드시 super.initState() 호출:
    • initState를 오버라이드할 때 Flutter의 기본 초기화 작업을 위해 super.initState()를 호출해야 합니다.
  3. dispose와 함께 사용:
    • 리스너나 애니메이션 컨트롤러를 initState에서 추가했다면, dispose 함수에서 반드시 정리(cleanup)해야 메모리 누수를 방지할 수 있습니다.
반응형
반응형

이번 팀 프로젝트는 도서관 플랫폼으로 주제를 정하고 사이트 이름은 북허브(BookHub)로 정하였습니다.

작업기간은 2024.02.13 ~ 2024.03.05 (총 15일) 소요되었습니다.

 

이번 프로젝트에서는 저는 부팀장역할을 맡아 사이트 전체 프론트 디자인 부분과

발표 PPT 제작 및

관리자페이지, 포인트 결제 기능부분을 맡아 개발을 진행하였습니다.

 

💡 프로젝트 선정 배경

  • 환경을 생각한 도서 서비스 → 책 생산시 낭비되는 종이를 막기 위해 책 공유 기능을 넣어 자원낭비를 최소화
  • 모임을 개설하여 서로의 의견을 주고 받을 수 있는 모임 서비스
  • 포인트 결제 수수료, 배너광고 수익을 통한 사이트 수익창출
  • 도서 검색, 대출/반납, 도서관 내 포인트를 이용한 베이커리, PC 사용 서비스

 

💡 내가 개발한 기능

  • 메인페이지 전체 디자인
  • 포트원 결제 API를 이용한 포인트상품 결제/환불 기능 구현(사이트 수익모델)
  • 관리자페이지 광고배너 등록/수정/삭제
  • 관리자페이지 상품 등록/수정/삭제

 

💡 기능 개발하면서 힘들었던점

  • 결제 환불 부분에서 직접 api를 만들면서 첫번째 환불 내역은 취소가 되는데 두번째...n번째 내역은 취소가 안되는 오류가 발생해 밤 낮으로 고민하고 검색하고 할 수 있는 방법을 찾은 결과 정상적으로 환불요청 구현을 완료하여서 뿌듯하였음

 

🛠️ 기술 스택

🔻 Front-End

  • HTML5
  • CSS3
  • JavaScript : 1.16.1
  • JQuery : 3.6.4
  • bootstrap : 4.6.2

🔻 Back-End

  • SpringBoot 3.1.8
  • JAVA 17
  • MySQL 8.0.26
  • lombok
  • Apache Tomcat : 9.0
  • MyBatis
  • JSP

🔻 API

  • OAuth2.0 (구글, 카카오, 네이버)
  • 포트원 결제 API
  • 카카오 주소 API

🔻 Collaboration

  • Git
  • GitBash
  • Discode
  • Notion

💡 전체 기능 시연 영상

📖 주요 담당 기능 미리보기

🔷 메인페이지

  • 웹 페이지를 개발하면서 가장 공들인 부분
  • 사이트 로고도 직접제작하여 실제로 운영하는 사이트 처럼 디자인함

 

🔷 포트원 API를 이용한 포인트 상품 결제

  • 저번 게임쇼핑몰 프로젝트에는 토스페이먼츠 api를 이용하였는데 확실히 포트원 api가 사용하기가 쉬웠다.

 

🔷 포트원 API를 이용한 포인트 상품 환불

  • 가장 시간을 많이 쓴 작업
  • 환불로직에 대한 이해가 부족해서 몇번이고 공식문서를 정독해서 완성한 기능이자 애착이 가는 기능

 

🔷 관리자 페이지 배너 등록

  • 수익 모델을 생각하다가, 배너 광고 클릭시 클릭 횟수마다 금액을 지정하면 괜찮을거 같아서 만든 기능
  • 한번 클릭시마다 550원이 광고수익으로 넘어간다.

 

 

🔷 관리자 페이지 상품 등록

  • 위에서 봤던 포인트 결제 상품들을 등록하는 관리자 페이지 부분
  • BookHub 포인트 상품을 추가하는 관리자 페이지 부분
  • 관리자 계정으로 로그인 후 포인트 관리 → 포인트 상품 등록 페이지에서 해당상품명, 포인트, 실제 결제될 가격 입력 후 등록
  • 메인 페이지 포인트 구매하기 페이지에 실제 등록된 상품이 보이게 구현

 

✅  총평

  • 1차 프로젝트이다 보니 부족한 부분이 많았고, 구현했다가 시간 부족으로 폐기한 기능들이 많아 아쉬웠다.
  • 팀원들끼리 남아서 서로 피드백하고 회의하면서 작업하는게 작업 퀄리티를 높힐 수 있어서 좋았다.
반응형
반응형

💡 SQL을 추상화한 JPQL

JPA : ORM(Object-Relational Mapping) 프레임워크

JPQL(Java Persistence Query Language)

JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어를 제공합니다.

따라서 테이블을 대상으로 쿼리 하는 것이 아닌 엔티티 객체를 대상으로 쿼리합니다.

JPQL은 SQL을 추상화했기 때문에 특정 데이터베이스 SQL에 의존하지 않는 장점이 있습니다.

JPQL은 SQL과 문법이 유사하며, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN을 지원합니다.


💡 쿼리메소드를 사용한 쿼리문 만들기

✅ TODO 1) 전체 조회 + 정렬(내림차순)

📂 DeptRepository.java

package com.example.jpacustomexam.repository;

import com.example.jpacustomexam.model.Dept;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * packageName : com.example.jpacustomexam.repository
 * fileName : DeptRepository
 * author : GGG
 * date : 2023-10-17
 * description : 부서 레포지토리 (기본 CRUD 함수)
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-17         GGG          최초 생성
 */
@Repository
public interface DeptRepository extends JpaRepository<Dept, Integer> {
    // 개발자가 직접 SQL 작성 하는 기능(JPQL) : 1) 쿼리메소드
    //                                      2) @Query 쓰는 방법
    // todo: 1) 전체조회 + 정렬(내림차순)
    List<Dept> findAllByOrderByDnoDesc();
}

📂 DeptService.java

package com.example.jpacustomexam.service;

import com.example.jpacustomexam.model.Dept;
import com.example.jpacustomexam.repository.DeptRepository;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * packageName : com.example.jpacustomexam.service
 * fileName : DeptService
 * author : GGG
 * date : 2023-10-17
 * description : 부서 서비스
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-17         GGG          최초 생성
 */
@Service
public class DeptService {
    @Autowired
    DeptRepository deptRepository;

    /** 전체조회 + 정렬(dno 내림차순) */
    // todo) 쿼리메소드를 사용한 함수
    public List<Dept> findAllByOrderByDnoDesc(){
        List<Dept> list = deptRepository.findAllByOrderByDnoDesc();
        return list;
    }
}

 

📂 DeptController.java

package com.example.jpacustomexam.controller;

import com.example.jpacustomexam.model.Dept;
import com.example.jpacustomexam.service.DeptService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * packageName : com.example.jpacustomexam.controller
 * fileName : DeptController
 * author : GGG
 * date : 2023-10-17
 * description : 부서 컨트롤러 (리액트 용)
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-17         GGG          최초 생성
 */
@Slf4j
@RestController
public class DeptController {
    @Autowired
    DeptService deptService;    // DI

    /** 전체 조회 + 정렬(dno 내림차순) */
    @GetMapping("/dept/desc")
    public ResponseEntity<Object> getDeptAllDesc(){
        try {
            // todo : 전체 조회 + 정렬(dno 내림차순)함수 호출
            List<Dept> list = deptService.findAllByOrderByDnoDesc();
            if (list.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(list, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }
        } catch (Exception e) {
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
}

 

✔ 서버 재시작 후 API 테스트 

부서번호가 내림차순으로 정렬된 것을 알 수 있습니다.

1) 쿼리메소드 : 함수이름으로 sql문 생성

예) findAllByOrderByDnoDesc

findAllByOrderByDnoDesc
findAll 전체조회
OrderBy 정렬
Dno 대상컬럼명
Desc 내림차순

 

💡 JPA의 쿼리메소드 샘플은 다음과 같습니다.

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

Distinct findDistinctByLastnameAndFirstname select distinct …​ where x.lastname = ?1 and x.firstname = ?2
And findByLastnameAndFirstname … where x.lastname = ?1 and x.firstname = ?2
Or findByLastnameOrFirstname … where x.lastname = ?1 or x.firstname = ?2
Is, Equals findByFirstname,findByFirstnameIs,findByFirstnameEquals … where x.firstname = ?1
Between findByStartDateBetween … where x.startDate between ?1 and ?2
LessThan findByAgeLessThan … where x.age < ?1
LessThanEqual findByAgeLessThanEqual … where x.age <= ?1
GreaterThan findByAgeGreaterThan … where x.age > ?1
GreaterThanEqual findByAgeGreaterThanEqual … where x.age >= ?1
After findByStartDateAfter … where x.startDate > ?1
Before findByStartDateBefore … where x.startDate < ?1
IsNull, Null findByAge(Is)Null … where x.age is null
IsNotNull, NotNull findByAge(Is)NotNull … where x.age not null
Like findByFirstnameLike … where x.firstname like ?1
NotLike findByFirstnameNotLike … where x.firstname not like ?1
StartingWith findByFirstnameStartingWith … where x.firstname like ?1 (parameter bound with appended %)
EndingWith findByFirstnameEndingWith … where x.firstname like ?1 (parameter bound with prepended %)
Containing findByFirstnameContaining … where x.firstname like ?1 (parameter bound wrapped in %)
OrderBy findByAgeOrderByLastnameDesc … where x.age = ?1 order by x.lastname desc
Not findByLastnameNot … where x.lastname <> ?1
In findByAgeIn(Collection<Age> ages) … where x.age in ?1
NotIn findByAgeNotIn(Collection<Age> ages) … where x.age not in ?1
True findByActiveTrue() … where x.active = true
False findByActiveFalse() … where x.active = false
IgnoreCase findByFirstnameIgnoreCase … where UPPER(x.firstname) = UPPER(?1)

✅ TODO 2) 전체 조회 + 정렬(내림차순) - 부서명으로 내림차순, 부서번호 오름차순

📂 DeptRepository.java 추가

    // todo: 2) 전체조회 + 정렬(부서명 내림차순)
    List<Dept> findAllByOrderByDnameDesc();

    // todo: 3) 전체조회 + 정렬(부서번호 오름차순) : Asc 생략가능
    List<Dept> findAllByOrderByDno();
}

📂 DeptService.java 추가

/** 전체조회 + 정렬(dname 내림차순) : 쿼리메소드 */
public List<Dept> findAllByOrderByDnameDesc(){
    List<Dept> list = deptRepository.findAllByOrderByDnameDesc();
    return list;
}

/** 전체조회 + 정렬(dno 오름차순) : 쿼리메소드 */
public List<Dept> findAllByOrderByDno(){
    List<Dept> list = deptRepository.findAllByOrderByDno();
    return list;
}

📂 DeptController.java 추가

 /** 전체 조회 + 정렬(dname 내림차순) */
    @GetMapping("/dept/dname/desc")
    public ResponseEntity<Object> findAllByOrderByDnameDesc(){
        try {
            // todo : 전체 조회 + 정렬(dno 내림차순)함수 호출
            List<Dept> list = deptService.findAllByOrderByDnameDesc();
            if (list.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(list, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }
        } catch (Exception e) {
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
    /** 전체 조회 + 정렬(dno 오름차순) */
    @GetMapping("/dept/dno/asc")
    public ResponseEntity<Object> findAllByOrderByDno(){
        try {
            // todo : 전체 조회 + 정렬(dno 내림차순)함수 호출
            List<Dept> list = deptService.findAllByOrderByDno();
            if (list.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(list, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }
        } catch (Exception e) {
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

✔ 서버 재시작 후 API 테스트 

1) 전체조회 - 부서이름으로 내림차순

2) 전체조회 - 부서번호 오름차순

✅ TODO 3) 전체조회 + dname(부서명) like 검색 + dname 내림차순 조회

📂 DeptRepository.java 추가

// todo: 4) dname like 검색(DnameContaining)+ dname 내림차순 조회
List<Dept> findAllByDnameContainingOrderByDnameDesc(String dname);

📂 DeptService.java 추가

/** dname(부서명) like 검색 + dname 내림차순 조회 */
public List<Dept> findAllByDnameContainingOrderByDnameDesc(String dname){
    List<Dept> list 
            = deptRepository.findAllByDnameContainingOrderByDnameDesc(dname);
    return list;
}

📂 DeptController.java 추가

 /** 전체조회 + dname(부서명) like 검색 + dname 내림차순 조회 */
    @GetMapping("/dept/dname/containing/desc/{dname}")
    public ResponseEntity<Object> findAllByDnameContainingOrderByDnameDesc(
            @PathVariable String dname
    ){
        try {
            // todo : 전체 조회 + 정렬(dno 내림차순)함수 호출
            List<Dept> list
                    = deptService
                    .findAllByDnameContainingOrderByDnameDesc(dname);
            if (list.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(list, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }
        } catch (Exception e) {
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

✔ 서버 재시작 후 API 테스트 

1) 전체조회 - 부서명 like 검색 + dname 내림차순 조회

✅ TODO 4) 쿼리메소드 응용 연습문제

// todo) 응용연습
//    todo: 응용 연습문제 
//    todo: 연습 4) EMP 테이블에서 Job 이 manager 이고
//              매개변수로 부서번호(dno)를 받는 함수를 작성하세요.

//    todo: 연습 5) Emp 테이블에서 salary 가 1000 ~ 1500 사이의 값을 같는
//     사원을 조회하려고 합니다.  함수를 작성해 주세요
더보기

📂 EmpRepository.java

 

List<Emp> findAllByJobAndDno(String job, int dno);

List<Emp> findAllBySalaryBetween(int first, int last);

📂 EmpService.java

//    todo: 연습 4) EMP 테이블에서 Job 이 manager 이고
//              매개변수로 부서번호(dno)를 받는 함수를 작성하세요.
public List<Emp> findAllByJobAndDno(String job, int dno){
    List<Emp> list = empRepository.findAllByJobAndDno(job, dno);
    return list;
}

//    todo: 연습 5) Emp 테이블에서 salary 가 1000 ~ 1500 사이의 값을 같는
//     사원을 조회하려고 합니다.  함수를 작성해 주세요
public List<Emp> findAllBySalaryBetween(int first, int last){
    List<Emp> list = empRepository.findAllBySalaryBetween(first, last);
    return list;
}

📂 EmpController.java

  /** 연습 4) : 쿼리메소드 */
    @GetMapping("/emp/dno/{dno}")
    public ResponseEntity<Object> findAllByEnameContaining(
            @PathVariable int dno
    ) {
        try {
//         전체 조회 + 정렬(dno 내림차순) 호출
            List<Emp> list
                    = empService.findAllByJobAndDno("MANAGER", dno);

            if (list.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(list, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }

        } catch (Exception e){
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    /** 연습5 */

    @GetMapping("emp/salary/{first}/{last}")
    public ResponseEntity<Object> findAllBySalaryBetween(@PathVariable int first, @PathVariable int last){
        try {
            List<Emp> list
                    = empService.findAllBySalaryBetween(first, last);
            if(list.isEmpty() == false){
                return new ResponseEntity<>(list, HttpStatus.OK);
            } else {
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }

        } catch (Exception e) {
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

✔ 서버 재시작 후 API 테스트 

연습 4)

 

연습 5)

[
  {
    "insertTime": "2023-10-17 06:38:45",
    "updateTime": null,
    "eno": 7521,
    "ename": "WARD",
    "job": "SALESMAN",
    "manager": 7698,
    "hiredate": "1981-02-22 00:00:00",
    "salary": 1250,
    "commission": 500,
    "dno": 30
  },
  {
    "insertTime": "2023-10-17 06:38:45",
    "updateTime": null,
    "eno": 7654,
    "ename": "MARTIN",
    "job": "SALESMAN",
    "manager": 7698,
    "hiredate": "1981-09-28 00:00:00",
    "salary": 1250,
    "commission": 1400,
    "dno": 30
  },
  {
    "insertTime": "2023-10-17 06:38:45",
    "updateTime": null,
    "eno": 7844,
    "ename": "TURNER",
    "job": "SALESMAN",
    "manager": 7698,
    "hiredate": "1981-09-08 00:00:00",
    "salary": 1500,
    "commission": 0,
    "dno": 30
  },
  {
    "insertTime": "2023-10-17 06:38:45",
    "updateTime": null,
    "eno": 7876,
    "ename": "ADAMS",
    "job": "CLERK",
    "manager": 7788,
    "hiredate": "1987-07-13 00:00:00",
    "salary": 1100,
    "commission": null,
    "dno": 20
  },
  {
    "insertTime": "2023-10-17 06:38:45",
    "updateTime": null,
    "eno": 7934,
    "ename": "MILLER",
    "job": "CLERK",
    "manager": 7782,
    "hiredate": "1982-01-23 00:00:00",
    "salary": 1300,
    "commission": null,
    "dno": 10
  }
]
응답 파일이 저장되었습니다.
> 2023-10-17T153953.200.json

Response code: 200; Time: 25ms (25 ms); Content length: 933 bytes (933 B)

💡 @Query 이용한 쿼리문 작성

데이터베이스에서 값을 가져올 때는 위의 코드처럼 메서드의 이름만으로 쿼리 메서드를 생성할 수 도 있습니다.

이번에는 @Query 어노테이션을 사용해 직접 JPQL을 작성해봅시다.

 

📂 DeptRepository.java

    //    ----------------------------------------
    //    @Query 예제
    //    1) 오라클 쿼리
    //    2) 객체 쿼리
    //    ----------------------------------------
    //    todo 1) ename like 검색
    @Query(value = "SELECT TD.* FROM TB_EMP TD WHERE TD.ENAME LIKE '%' || :dname || '%'", nativeQuery = true)
    List<Emp> selectByEname(@Param("ename") String ename);

위 코드를 다르게 작성하는 방법(JDK 버전에 따라 사용 불가능할 수도 있음)

//    todo: 1-1) 위의 코드를 다르게 코딩
    @Query(value = "SELECT TD.* FROM TB_DEPT TD WHERE TD.DNAME LIKE '%'||:dname||'%'",
            nativeQuery = true)
    List<Dept> selectByDname(String dname);
//    todo: 1-2) 위의 코드를 다르게 코딩 : 참고
@Query(value = "SELECT TD.* FROM TB_DEPT TD WHERE TD.DNAME LIKE '%'|| ?1 ||'%'",
        nativeQuery = true)
List<Dept> selectByDname(String dname);

✅ SQL에서 쿼리문 결과가 올바른지 확인

📂 DeptService.java

/** 전체조회 + dname like 검색 : @Query */
public List<Dept> selectByDname(String dname) {
    List<Dept> list
            = deptRepository.selectByDname(dname);

    return list;
}

📂 DeptController.java

 /** 전체조회 + dname like 검색 : @Query */
    @GetMapping("/dept/dname/{dname}")
    public ResponseEntity<Object> selectByDname(
            @PathVariable String dname
    ) {
        try {
//         전체 조회 + 정렬(dno 오름차순) 호출
            List<Dept> list
                    = deptService.selectByDname(dname);

            if (list.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(list, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }

        } catch (Exception e){
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

✅ 실행결과

부서명 중에 'S'가 들어가는 부서명 검색하기

GET http://localhost:8000/dept/dname/S

HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 18 Oct 2023 00:11:16 GMT
Keep-Alive: timeout=60
Connection: keep-alive

[
  {
    "insertTime": "2023-10-18 00:11:06",
    "updateTime": null,
    "dno": 20,
    "dname": "RESEARCH",
    "loc": "DALLAS"
  },
  {
    "insertTime": "2023-10-18 00:11:06",
    "updateTime": null,
    "dno": 30,
    "dname": "SALES",
    "loc": "CHICAGO"
  },
  {
    "insertTime": "2023-10-18 00:11:06",
    "updateTime": null,
    "dno": 40,
    "dname": "OPERATIONS",
    "loc": "BOSTON"
  }
]
응답 파일이 저장되었습니다.
> 2023-10-18T091116.200.json

Response code: 200; Time: 277ms (277 ms); Content length: 295 bytes (295 B)

✅ 객체쿼리를 이용한 쿼리문 작성

//    todo: 2) dname like 검색 (객체 쿼리)
    // 객체 쿼리 만드는 방법 : 테이블 -> 클래스명
    //                      컬럼명 -> 속성명
    //                      * -> 사용하지 않음, 클래스명의 별칭을 붙여서 사용
    //                      (대소문자를 구분함, nativeQuery=true(생략))
    @Query(value = "SELECT TD FROM Dept TD WHERE TD.dname LIKE '%'||:dname||'%'")
    List<Dept> selectByDname(@Param("dname") String dname);

 

✅ TODO) 부서 테이블에서 부서명(dname)과 위치(loc)를 매개변수로 받아 조회하는 함수 만들기

📂 DeptRepository.java

@Query(value = "SELECT D.* FROM TB_DEPT D " +
        "WHERE D.DNAME = :dname " +
        "AND   D.LOC = :loc ", nativeQuery = true)
List<Dept> selectByDnameAndLoc(@Param("dname") String dname,@Param("loc") String loc);

📂 DeptService.java

/** 전체조회 + dname like 검색 : @Query */
public List<Dept> selectByDnameAndLoc(String dname, String loc) {
    List<Dept> list
            = deptRepository.selectByDnameAndLoc(dname, loc);

    return list;
}

📂 DeptController.java

    /** 전체조회 + dname like 검색 : @Query */
    @GetMapping("/dept/dname/{dname}/loc/{loc}")
    public ResponseEntity<Object> selectByDnameAndLoc(
            @PathVariable String dname,
            @PathVariable String loc
    ) {
        try {
//         전체 조회 + 정렬(dno 오름차순) 호출
            List<Dept> list
                    = deptService.selectByDnameAndLoc(dname, loc);

            if (list.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(list, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }

        } catch (Exception e){
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

 

반응형

'Spring Boot > 스프링부트 예제' 카테고리의 다른 글

JPA - 연관관계 매핑  (0) 2023.10.19
JPA 페이징 처리  (1) 2023.10.18
JPA 상세조회, 저장함수, 수정함수, 삭제함수  (0) 2023.10.17
JPA를 활용한 CRUD 구현하기  (0) 2023.10.16
JSTL 라이브러리  (0) 2023.10.06
반응형

💡 파라미터 방식

앞 전 예제에서는 쿼리스트링 방식으로 매개변수를 전달하였습니다. 이번에는 쿼리스트링 방식을 개선한 파라미터 방식의 매개변수 전달 방법에 대해 알아보겠습니다.

 

📂PathVariableController.java

@Controller
@RequestMapping("/exam05")
public class PathVariableController {
//    TODO : url 테스트 파라미터 방식 : http://localhost:8000/exam05/path-variable/LeeJunHyuk
//                                  => @GetMapping("/path-variable/{웹매개변수명}")
//                                  => 웹 브라우저 주소창 사용 : url/값
//    TODO : url 테스트 쿼리스트링 방식 : http://localhost:8000/exam05/path-variable?name=LeeJunHyuk
    @GetMapping("/path-variable/{name}")
    public String getPathVariable(
            @PathVariable String name,
            Model model)
    {
        model.addAttribute("name", name);
        return "exam05/path_variable.jsp";
    }
}

📂path_variable.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%--TODO : JSTL 표현식 사용을 위한 import--%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<%-- TODO : el 표현식--%>
    <p>${name}</p>
</body>
</html>

파라미터 형식의 매개변수 전달방법은

@GetMapping("url/{웹매개변수명}") 으로 사용하고 메소드 매개변수로 @PathVariable를 사용한 것을 알 수 있습니다.

 

📌 실행결과

반응형

+ Recent posts