2026년 9월 10일 목요일

[oracle]옵티마이저(Optimizer)와 실행 계획, 실전 튜닝 기초 완벽 정리

[Oracle SQL] 옵티마이저(Optimizer)와 실행 계획, 실전 SQL 튜닝 가이드

이 가이드에서는 오라클 데이터베이스의 핵심 엔진인 옵티마이저의 원리를 이해하고, DBeaver와 SQL Developer를 활용하여 실제 쿼리의 실행 계획을 분석 및 튜닝하는 전체 과정을 안내합니다.


Part 1: 옵티마이저(Optimizer)의 이해와 작동 원리

가장 먼저 옵티마이저가 쿼리의 '가장 빠르고 효율적인 길(실행 계획)'을 찾아내는 4단계 과정을 이해해야 합니다.

1단계: 쿼리 변환 및 후보군 생성

데이터베이스는 사용자가 작성한 SQL을 처리하기 쉬운 형태로 몰래 재작성(Query Transformation)한 뒤, 가능한 모든 조인 방식과 스캔 방식의 경우의 수(Plan Generation)를 도출합니다.

2단계: 비용 산정 및 실행 계획 선택

DB가 평소 수집해 둔 통계 정보를 바탕으로 각 경로의 예상 작업 비용(Cost)을 계산하고, 총비용이 가장 낮은 최적의 경로를 최종 실행 계획으로 확정합니다.


Part 2: 오라클 힌트(Hint) 실무 사용법

옵티마이저가 낡은 통계 정보 때문에 비효율적인 경로를 선택할 때, 개발자가 직접 올바른 길을 지시하는 '강제 명령어’입니다.

🚨 주의 사항:
FROM 절에서 테이블에 별칭(Alias)을 주었다면, 힌트 안에서도 반드시 원본 테이블명이 아닌 별칭을 사용해야 적용됩니다.

1단계: 인덱스 강제 적용 (INDEX)

가장 자주 쓰이는 힌트로, 쿼리가 전체 스캔을 타고 있을 때 특정 인덱스를 타도록 강제합니다.

index_hint.sql

-- EMPLOYEES 테이블(E)의 IDX_EMP_NAME 인덱스를 사용하도록 강제
SELECT /*+ INDEX(E IDX_EMP_NAME) */ 
    EMP_NO, EMP_NAME 
FROM EMPLOYEES E 
WHERE EMP_NAME = '홍길동';

#### **2단계: 테이블 전체 스캔 강제 (FULL)**

데이터량이 적거나, 조회해야 할 데이터가 전체의 15% 이상일 때는 인덱스보다 전체 스캔이 빠릅니다.

**`full_scan_hint.sql`**

SQL

– 인덱스를 타지 않고 BIG_SALES 테이블(S) 전체를 읽도록 강제
SELECT /*+ FULL(S) */
SALE_DATE, SUM(AMOUNT)
FROM BIG_SALES S
GROUP BY SALE_DATE;


#### **3단계: 조인 방식 제어 (USE_NL, USE_HASH)**

조인되는 데이터의 양에 따라 Nested Loop 조인(소량) 또는 Hash 조인(대량)을 강제합니다.

**`join_hint.sql`**

SQL

– O와 C 테이블을 대용량 처리에 유리한 해시 조인으로 연결
SELECT /*+ USE_HASH(O C) */
O.ORDER_ID, C.CUSTOMER_NAME
FROM ORDERS O
JOIN CUSTOMERS C ON O.CUST_ID = C.CUST_ID;


### **Part 3: SQL 툴에서 실행 계획(Execution Plan) 확인**

작성한 쿼리와 힌트가 옵티마이저에게 잘 전달되었는지 확인하기 위해 실행 계획을 띄워봅니다.

#### **1단계: DBeaver에서 확인하기**

1.  실행할 쿼리를 마우스로 드래그하여 블록 지정합니다.
    
2.  단축키 **`Ctrl + Shift + E`** (Mac은 `Cmd + Shift + E`)를 누릅니다.
    
3.  하단 탭에 트리(Tree) 형태로 쿼리의 실행 순서와 Cost가 출력됩니다.
    

#### **2단계: SQL Developer에서 확인하기**

1.  실행할 쿼리를 클릭하거나 블록 지정합니다.
    
2.  단축키 `F10`을 누릅니다.
    
3.  하단 질의 결과 창에 '실행 계획' 탭이 열리며 상세 트리가 나타납니다.
    

#### **3단계: SQL 쿼리로 직접 확인하기 (툴이 없을 때)**

**`explain_plan.sql`**

SQL

– 1. 쿼리 앞에 EXPLAIN PLAN FOR를 붙여 메모리에 실행계획 생성
EXPLAIN PLAN FOR
SELECT * FROM EMPLOYEES E WHERE DEPT_NO = 10;

– 2. 생성된 실행계획을 테이블 형태로 출력하여 확인
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);


### **Part 4: 실행 계획 읽는 법과 실전 튜닝 공식**

출력된 실행 계획 트리를 바탕으로 병목 구간을 찾고 튜닝을 진행합니다.

#### **1단계: 범인 찾기 및 실행 순서 파악**

-   **순서 읽기:** 안에서 밖으로(들여쓰기가 깊은 것부터), 위에서 아래로(형제 노드 중엔 위에 있는 것부터) 읽습니다.
    
-   **병목 찾기:** `Cost`가 비정상적으로 높거나, 대용량 테이블인데 `TABLE ACCESS FULL`(전체 스캔)이 발생하는 구간을 찾습니다.
    

#### **2단계: 상황 파악하기 (데이터 크기 확인)**

전체 스캔이 무조건 나쁜 것은 아닙니다. 테이블의 전체 데이터 건수와 WHERE 조건으로 필터링되어 나오는 실제 예상 추출 건수(Rows)의 비율을 확인합니다.

#### **3단계: 해결책 결정 (Index vs Full Scan)**

-   **인덱스가 유리한 경우:** 조건에 맞는 데이터가 **전체 데이터의 10~15% 이하**일 때. (인덱스 힌트 부여 또는 인덱스 생성)
    
-   **FULL SCAN이 유리한 경우:** 데이터가 몇천 건 이하로 아주 작거나, 추출 데이터가 **전체의 15% 이상**일 때.
    

### **Part 5: Cost(비용)의 진실과 통계 정보 갱신**

Cost 수치 자체보다 중요한 것은 예상 추출 건수와의 '가성비'입니다.

#### **1단계: 악성 쿼리 판별하기**

예상 추출 데이터(`Rows`)는 고작 5건인데, `Cost`(비용)가 50,000이라면 모래사장에서 바늘 찾기를 위해 DB 자원을 낭비하는 최악의 상태입니다. 반대로 Rows가 150만 건인데 Cost가 50,000이라면 정상적입니다.

#### **2단계: 테이블 통계 정보 수동 갱신 (최종 튜닝)**

인덱스도 정상이고 힌트도 줬는데 쿼리가 엉뚱하게 돈다면, 데이터베이스가 가진 지형도(통계 정보)가 옛날 버전일 확률이 높습니다. 이때는 DBA 권한으로 통계 정보를 최신화해줍니다.

**`gather_stats.sql`**

SQL

– XXXXX 테이블의 통계 정보를 최신 상태로 수집하여 옵티마이저에게 제공
EXEC DBMS_STATS.GATHER_TABLE_STATS(‘계정명’, 'XXXXX ');

2025년 12월 21일 일요일

[OS]리눅스서버 WAS 관련 권한 관리

[Best Practice] Linux 서버 WAS 권한 체계 구축 가이드

리눅스 환경에서 다수의 운영자가 WAS(Tomcat, Nginx 등)를 공동 관리할 때 발생하는 권한 꼬임(Permission Denied) 문제를 방지하기 위한 표준 설정법입니다.


1. 계정 및 그룹 설계 (Account & Group)

리눅스는 ‘1파일 1그룹’ 원칙을 가지므로, 모든 관리자를 하나의 **보조 그룹(Secondary Group)**으로 묶는 전략이 필수입니다.

🛠 터미널(Terminal) 명령어

Bash

# 1. WAS 관리 전용 그룹 생성
sudo groupadd webadmin

# 2. 서비스 구동용 시스템 계정 생성 (로그인 불가 설정으로 보안 강화)
sudo useradd -r -s /usr/sbin/nologin wasuser

# 3. 관리자 계정(aaaa, bbbb)을 관리 그룹에 추가 (보조 그룹 추가 옵션 -aG)
sudo usermod -aG webadmin aaaa
sudo usermod -aG webadmin bbbb
sudo usermod -aG webadmin wasuser  # 서비스 계정도 그룹에 포함


2. 폴더 소유권 및 특수 권한 설정 (Ownership & SetGID)

리눅스에는 윈도우식 상속이 없으므로, SetGID라는 특수 권한을 사용하여 새로 생성되는 파일의 그룹 소유권을 강제로 고정합니다.

🛠 설정 절차

  1. 디렉토리 소유권 변경:

    Bash

    # /app/was 폴더의 소유자를 wasuser로, 그룹을 webadmin으로 일괄 변경
    sudo chown -R wasuser:webadmin /app/was
    
    
  2. 기본 권한 부여:

    Bash

    # 소유자와 그룹 멤버에게 읽기/쓰기/실행 권한 부여 (775)
    sudo chmod -R 775 /app/was
    
    
  3. SetGID 설정 (리눅스 상속의 핵심):

    Bash

    # 하위 디렉토리에 SetGID를 걸어, 누가 파일을 만들어도 그룹이 'webadmin'으로 고정되게 함
    find /app/was -type d -exec chmod g+s {} +
    
    

3. 서비스 구성 (Systemd Service)

시스템 부팅 시 WAS가 지정된 계정으로 자동 실행되도록 systemd 서비스 파일을 설정합니다.

🛠 서비스 파일 예시 (/etc/systemd/system/tomcat.service)

Ini, TOML

[Unit]
Description=Apache Tomcat Web Application Container
After=network.target

[Service]
Type=forking

# 핵심 설정: 서비스를 구동할 사용자 및 그룹 명시
User=wasuser
Group=webadmin

Environment=JAVA_HOME=/usr/lib/jvm/java-11-openjdk
ExecStart=/app/was/tomcat/bin/startup.sh
ExecStop=/app/was/tomcat/bin/shutdown.sh

[Install]
WantedBy=multi-user.target


4. 💡 실무 핵심 요약 (Cheat Sheet)

항목

설정 권장 사항 (Best Practice)

이유

그룹 관리

모든 관리자를 보조 그룹(webadmin)에 포함

다수의 사용자가 동일한 파일 수정 가능

상속 구현

폴더에 SetGID(g+s) 권한 부여

신규 생성 파일의 그룹 소유권 자동 고정

파일 생성값

umask 값 확인 (기본 002 권한 권장)

그룹 멤버에게 쓰기 권한이 자동으로 생기도록 함

권한 적용

그룹 추가 후 로그아웃 & 로그인 필수

세션이 갱신되어야 새로운 그룹 권한이 반영됨


⚠️ 주의사항

  • umask 설정: 시스템의 umask022로 되어 있으면 그룹 멤버에게 쓰기 권한이 없습니다. 그룹 협업이 잦은 WAS 폴더 내에서는 umask 002를 권장합니다.

  • 파일 vs 폴더: chmod g+s는 반드시 디렉토리에만 적용하는 것이 보안상 안전합니다. (find 명령어 활용 권장)


[OS]윈도우서버 WAS 관련 권한관리

[Best Practice] Windows 서버 WAS 권한 체계 구축 가이드

Windows 환경에서 WAS(Tomcat, JBoss, IIS 등)를 안정적으로 운영하기 위해 **‘소유권 분쟁’**과 ‘액세스 거부’ 에러를 원천 차단하는 표준 보안 설정법입니다.


1. 계정 및 그룹 설계 (Account Design)

개인 계정이 아닌 서비스 전용 계정권한 관리용 그룹을 분리하여 관리 효율성과 보안성을 동시에 확보합니다.

🛠 관리자 명령 프롬프트(CMD) 명령어

DOS

:: 1. WAS 관리 전용 로컬 그룹 생성
net localgroup WAS_Managers /add

:: 2. 서비스 구동용 계정 생성 (일반 사용자 권한)
:: /passwordchg:no 옵션으로 서비스 중단 방지
net user svc_was P@ssword123 /add /comment:"WAS Service Account" /passwordchg:no

:: 3. 관리용 그룹에 멤버 추가 (서비스 계정 + 실제 운영자들)
net localgroup WAS_Managers svc_was /add
net localgroup WAS_Managers aaaa /add
net localgroup WAS_Managers bbbb /add


2. 폴더 권한 및 상속 설정 (ACL & Inheritance)

Windows 보안의 핵심인 **‘권한 상속’**을 활용하여, 서버 구동 중 생성되는 로그나 임시 파일이 소유권 문제로 인해 삭제/수정되지 않는 상황을 방지합니다.

🛠 설정 절차 (GUI 중심)

  1. 루트 폴더 생성: C:\was (모든 WAS 인스턴스의 부모 경로)

  2. 보안 속성 진입: C:\was 우클릭 → [속성][보안] 탭 → [고급] 클릭

  3. 권한 부여:

    • [추가] 클릭 → [보안 주체 선택]WAS_Managers 입력 후 확인

    • 권한: 모든 권한 또는 수정(Modify) 체크

    • 적용 대상: 이 폴더, 하위 폴더 및 파일 선택

  4. 상속 강제 적용 (가장 중요):

    • 창 하단의 [모든 자식 개체 사용 권한 항목을 이 개체의 상속 가능한 사용 권한 항목으로 바꾸기] 체크

    • [확인]을 눌러 하위의 모든 tomcat, logs 폴더에 권한을 일괄 전파


3. 서비스 로그온 권한 부여 (Service Logon Right)

생성한 svc_was 계정이 일반 사용자 권한임에도 불구하고 시스템 서비스를 구동할 수 있도록 권한을 승인합니다.

🛠 로컬 보안 정책 설정

  1. Win + Rsecpol.msc 실행

  2. 로컬 정책사용자 권한 할당 이동

  3. [서비스로 로그온] 항목 더블 클릭

  4. svc_was 계정을 명단에 추가 후 확인


4. WAS 서비스 실행 설정 (Service Configuration)

이제 실제 등록된 서비스를 특정 계정으로 구동하도록 연결합니다.

  1. services.msc 실행 → 해당 WAS 서비스(예: Tomcat) 우클릭 → [속성]

  2. [로그온] 탭 클릭 → [다음 계정으로 지정] 선택

  3. svc_was 계정과 비밀번호 입력 후 서비스 재시작


💡 실무 핵심 요약 (Cheat Sheet)

항목

설정 권장 사항 (Best Practice)

이유

권한 주체

개인 계정이 아닌 전용 그룹에 부여

멤버 교체 시 폴더 권한 수정 불필요

상속 설정

부모 폴더에서 상속 강제 적용

새로 생성되는 로그 파일의 접근권 보장

계정 권한

서비스 계정은 Standard User 유지

해킹 시 시스템 전체 장악 방지 (최소 권한)

수동 실행

관리자가 직접 실행 시 그룹 추가 필수

그룹 추가 후에는 반드시 재로그인 필요


2025년 7월 30일 수요일

[springboot]실제 JWT 발급 및 검증 구현

실제 JWT 발급 및 검증 구현

이전 단계에서 만든 임시 토큰을 실제 암호화된 JWT(JSON Web Token)로 대체하고, Spring Security 필터를 통해 API 요청을 보호하는 방법을 구현합니다.

Part 1: 백엔드 (Spring Boot) JWT 로직 구현

1단계: JWT Secret Key 설정

[수정] JWT 라이브러리의 보안 요구사항(256비트 이상)을 만족하는 더 길고 안전한 Secret Key 예시로 변경합니다.

backend/src/main/resources/application.properties 파일에 아래 내용을 추가합니다.

# JWT Secret Key 설정
# 256비트 이상의 길이를 가진 Base64 인코딩된 문자열이어야 합니다.
# 아래는 예시이며, 실제 운영 환경에서는 openssl rand -base64 32 명령 등으로 생성한 키를 사용하세요.
jwt.secret=256비트보타작은키는 오류

2단계: JWT 유틸리티 클래스 생성

JwtUtil.java

package dev.nerobong2.openehr.open_ehr.backend.jwt;
// ... (이전과 동일한 코드)

3단계: AuthService 수정

AuthService.java

package dev.nerobong2.openehr.open_ehr.backend.auth;

import dev.nerobong2.openehr.open_ehr.backend.jwt.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class AuthService {

    private final AuthenticationManager authenticationManager;
    private final JwtUtil jwtUtil;

    public String login(String username, String password) {
        // Spring Security를 통해 인증 시도
        Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(username, password)
        );

        // 인증 성공 시 UserDetails 객체를 가져와 JWT 생성
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        return jwtUtil.generateToken(userDetails);
    }
}

4단계: JWT 인증 필터 생성

JwtAuthFilter.java

package dev.nerobong2.openehr.open_ehr.backend.jwt;
// ... (이전과 동일한 코드)

5단계: SecurityConfig와 ApplicationConfig 분리

ApplicationConfig.java

package dev.nerobong2.openehr.open_ehr.backend.config;

import dev.nerobong2.openehr.open_ehr.backend.user.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.ArrayList;

@Configuration
@RequiredArgsConstructor
public class ApplicationConfig {

    private final UserRepository userRepository;

    @Bean
    public UserDetailsService userDetailsService() {
        return username -> userRepository.findByUsername(username)
                .map(user -> new org.springframework.security.core.userdetails.User(
                        user.getUsername(),
                        user.getPassword(),
                        new ArrayList<>()))
                .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService());
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

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

SecurityConfig.java

package dev.nerobong2.openehr.open_ehr.backend.config;

import dev.nerobong2.openehr.open_ehr.backend.jwt.JwtAuthFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthFilter jwtAuthFilter;
    private final AuthenticationProvider authenticationProvider;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .anyRequest().authenticated()
            )
            .authenticationProvider(authenticationProvider)
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

6단계: 인증 테스트용 API 생성

TestController.java

package dev.nerobong2.openehr.open_ehr.backend.test;
// ... (이전과 동일한 코드)

Part 2: 프론트엔드 (Vue) JWT 연동 및 테스트

(이전과 동일)

1단계: 상태 관리 (Pinia) 설정

frontend/src/stores/auth.ts

// ... (이전과 동일한 코드)

2단계: 인증된 API 호출 테스트

frontend/src/views/HomeView.vue

<script setup lang="ts">
// ... (이전과 동일한 코드)
</script>

<template>
  <main>
    <h1>메인 페이지</h1>
    <p>로그인에 성공했습니다.</p>
    <hr />
    <p><strong>{{ message }}</strong></p>
  </main>
</template>

다음 단계: 이제 백엔드 서버를 재시작하시면 오류 없이 정상적으로 실행될 것입니다. 그 후 다시 로그인을 테스트해 보세요.

2025년 7월 29일 화요일

[springboot]JWT 로그인 인증 구현 가이드

JWT 로그인 인증 구현 가이드

이 가이드에서는 Spring Boot와 Vue.js를 사용하여 JWT(JSON Web Token) 기반의 로그인 인증 시스템을 구축하는 전체 과정을 안내합니다.

Part 1: 백엔드 (Spring Boot) 설정

가장 먼저 백엔드에서 사용자 인증을 처리하고 JWT를 발급하는 API를 만듭니다.

1단계: 의존성 추가

(이전과 동일)

// build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
    // ...
}

2단계: User Entity 및 Repository 생성

(이전과 동일)

User.java

package dev.nerobong2.openehr.open_ehr.backend.user;
// ... (Lombok, JPA 어노테이션 등)
public class User {
    // ... id, username, password 필드 ...
}

UserRepository.java

package dev.nerobong2.openehr.open_ehr.backend.user;
// ...
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
}

3단계: Spring Security 설정 (단순 버전으로 복원)

403 오류의 원인을 명확히 하기 위해, 가장 단순한 초기 버전으로 설정을 되돌립니다.

SecurityConfig.java

// [수정] 403 오류 원인 파악을 위해 가장 단순한 버전으로 복원합니다.
package dev.nerobong2.openehr.open_ehr.backend.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

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

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // CSRF 보호 비활성화
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션 사용 안 함
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll() // 로그인/회원가입 API는 모두 허용
                .anyRequest().authenticated() // 나머지 요청은 인증 필요
            );
        return http.build();
    }
}

4단계: 로그인 API 생성 (단순 버전으로 복원)

AuthService에서 직접 PasswordEncoder를 사용하여 비밀번호를 비교하는 초기 방식으로 되돌립니다.

AuthService.java

// [수정] PasswordEncoder를 직접 사용하는 단순 버전으로 복원합니다.
package dev.nerobong2.openehr.open_ehr.backend.auth;

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import dev.nerobong2.openehr.open_ehr.backend.user.User;
import dev.nerobong2.openehr.open_ehr.backend.user.UserRepository;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class AuthService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public String login(String username, String password) {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));

        // 입력된 비밀번호와 DB의 암호화된 비밀번호를 비교
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
        }

        // 로그인 성공 시 임시 토큰 반환
        return "temp-jwt-token-for-" + user.getUsername();
    }
}

AuthController.java (변경 없음)

// ...
public class AuthController {
    // ...
    @PostMapping("/login")
    public String login(@RequestBody LoginRequestDto requestDto) {
        return authService.login(requestDto.username(), requestDto.password());
    }
}
record LoginRequestDto(String username, String password) {}

5단계: 올바른 테스트 데이터 생성 (매우 중요!)

“비밀번호가 일치하지 않습니다” 오류는 DB에 저장된 암호화된 비밀번호가 잘못되었기 때문입니다. 아래 방법으로 올바른 암호화 비밀번호를 직접 생성하여 DB에 넣으세요.

  1. 메인 애플리케이션 파일(OpenEhrBackendApplication.java)을 열고, 기존 클래스 내부에 임시로 아래 코드를 추가합니다. 이 코드는 서버가 시작될 때 비밀번호 '1234’를 암호화하여 콘솔에 출력해 줍니다.

    OpenEhrBackendApplication.java

    package dev.nerobong2.openehr.open_ehr.backend;
    
    import org.springframework.boot.CommandLineRunner;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.annotation.Bean;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    @SpringBootApplication
    public class OpenEhrBackendApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(OpenEhrBackendApplication.class, args);
        }
    
        // [추가] 비밀번호 암호화를 위해 이 Bean을 기존 클래스 내부에 임시로 추가합니다.
        @Bean
        public CommandLineRunner commandLineRunner(PasswordEncoder passwordEncoder) {
            return args -> {
                String encodedPassword = passwordEncoder.encode("1234");
                System.out.println("####################################################");
                System.out.println("Encoded password for '1234': " + encodedPassword);
                System.out.println("####################################################");
            };
        }
    }
    
    
  2. 백엔드 서버를 재시작합니다.

  3. 터미널(콘솔)에 아래와 같이 암호화된 비밀번호가 출력됩니다.

    ####################################################
    Encoded password for '1234': $2a$10$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    ####################################################
    
    
  4. 콘솔에 출력된 $2a$10$...로 시작하는 전체 문자열을 복사합니다.

  5. DBeaver에서 아래 SQL을 실행하여 password 컬럼에 복사한 값을 붙여넣고 사용자를 생성합니다.

    -- 기존 데이터 삭제
    DELETE FROM users WHERE username = 'testuser';
    
    -- 새로 생성된 암호화 값으로 INSERT
    INSERT INTO users (username, password, employee_number, company_code)
    VALUES ('testuser', '콘솔에서 복사한 암호화된 비밀번호 붙여넣기', '12345', 'COMP01');
    
    COMMIT;
    
    
  6. 테스트 데이터 생성이 끝나면, OpenEhrBackendApplication.java에 추가했던 CommandLineRunner 코드는 다시 삭제해도 됩니다.

Part 2: 프론트엔드 (Vue) 설정

(이전과 동일)

다음 단계: 이제 백엔드 서버를 재시작하고, 프론트엔드에서 아이디 testuser, 비밀번호 1234로 다시 로그인을 시도해 보세요. 이제는 403 오류 없이 정상적으로 로그인되어 메인 페이지로 이동할 것입니다.## JWT 로그인 인증 구현 가이드

이 가이드에서는 Spring Boot와 Vue.js를 사용하여 JWT(JSON Web Token) 기반의 로그인 인증 시스템을 구축하는 전체 과정을 안내합니다.

Part 1: 백엔드 (Spring Boot) 설정

가장 먼저 백엔드에서 사용자 인증을 처리하고 JWT를 발급하는 API를 만듭니다.

1단계: 의존성 추가

(이전과 동일)

// build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
    // ...
}

2단계: User Entity 및 Repository 생성

(이전과 동일)

User.java

package dev.nerobong2.openehr.open_ehr.backend.user;
// ... (Lombok, JPA 어노테이션 등)
public class User {
    // ... id, username, password 필드 ...
}

UserRepository.java

package dev.nerobong2.openehr.open_ehr.backend.user;
// ...
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
}

3단계: Spring Security 설정 (단순 버전으로 복원)

403 오류의 원인을 명확히 하기 위해, 가장 단순한 초기 버전으로 설정을 되돌립니다.

SecurityConfig.java

// [수정] 403 오류 원인 파악을 위해 가장 단순한 버전으로 복원합니다.
package dev.nerobong2.openehr.open_ehr.backend.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

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

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // CSRF 보호 비활성화
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션 사용 안 함
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll() // 로그인/회원가입 API는 모두 허용
                .anyRequest().authenticated() // 나머지 요청은 인증 필요
            );
        return http.build();
    }
}

4단계: 로그인 API 생성 (단순 버전으로 복원)

AuthService에서 직접 PasswordEncoder를 사용하여 비밀번호를 비교하는 초기 방식으로 되돌립니다.

AuthService.java

// [수정] PasswordEncoder를 직접 사용하는 단순 버전으로 복원합니다.
package dev.nerobong2.openehr.open_ehr.backend.auth;

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import dev.nerobong2.openehr.open_ehr.backend.user.User;
import dev.nerobong2.openehr.open_ehr.backend.user.UserRepository;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class AuthService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public String login(String username, String password) {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));

        // 입력된 비밀번호와 DB의 암호화된 비밀번호를 비교
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
        }

        // 로그인 성공 시 임시 토큰 반환
        return "temp-jwt-token-for-" + user.getUsername();
    }
}

AuthController.java (변경 없음)

// ...
public class AuthController {
    // ...
    @PostMapping("/login")
    public String login(@RequestBody LoginRequestDto requestDto) {
        return authService.login(requestDto.username(), requestDto.password());
    }
}
record LoginRequestDto(String username, String password) {}

5단계: 올바른 테스트 데이터 생성 (매우 중요!)

“비밀번호가 일치하지 않습니다” 오류는 DB에 저장된 암호화된 비밀번호가 잘못되었기 때문입니다. 아래 방법으로 올바른 암호화 비밀번호를 직접 생성하여 DB에 넣으세요.

  1. 메인 애플리케이션 파일(OpenEhrBackendApplication.java)을 열고, 기존 클래스 내부에 임시로 아래 코드를 추가합니다. 이 코드는 서버가 시작될 때 비밀번호 '1234’를 암호화하여 콘솔에 출력해 줍니다.

    OpenEhrBackendApplication.java

    package dev.nerobong2.openehr.open_ehr.backend;
    
    import org.springframework.boot.CommandLineRunner;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.annotation.Bean;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    @SpringBootApplication
    public class OpenEhrBackendApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(OpenEhrBackendApplication.class, args);
        }
    
        // [추가] 비밀번호 암호화를 위해 이 Bean을 기존 클래스 내부에 임시로 추가합니다.
        @Bean
        public CommandLineRunner commandLineRunner(PasswordEncoder passwordEncoder) {
            return args -> {
                String encodedPassword = passwordEncoder.encode("1234");
                System.out.println("####################################################");
                System.out.println("Encoded password for '1234': " + encodedPassword);
                System.out.println("####################################################");
            };
        }
    }
    
    
  2. 백엔드 서버를 재시작합니다.

  3. 터미널(콘솔)에 아래와 같이 암호화된 비밀번호가 출력됩니다.

    ####################################################
    Encoded password for '1234': $2a$10$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    ####################################################
    
    
  4. 콘솔에 출력된 $2a$10$...로 시작하는 전체 문자열을 복사합니다.

  5. DBeaver에서 아래 SQL을 실행하여 password 컬럼에 복사한 값을 붙여넣고 사용자를 생성합니다.

    -- 기존 데이터 삭제
    DELETE FROM users WHERE username = 'testuser';
    
    -- 새로 생성된 암호화 값으로 INSERT
    INSERT INTO users (username, password, employee_number, company_code)
    VALUES ('testuser', '콘솔에서 복사한 암호화된 비밀번호 붙여넣기', '12345', 'COMP01');
    
    COMMIT;
    
    
  6. 테스트 데이터 생성이 끝나면, OpenEhrBackendApplication.java에 추가했던 CommandLineRunner 코드는 다시 삭제해도 됩니다.

Part 2: 프론트엔드 (Vue) 설정

(이전과 동일)

다음 단계: 이제 백엔드 서버를 재시작하고, 프론트엔드에서 아이디 testuser, 비밀번호 1234로 다시 로그인을 시도해 보세요. 이제는 403 오류 없이 정상적으로 로그인되어 메인 페이지로 이동할 것입니다.

2024년 10월 25일 금요일

[oracle]백업및 복구

[oracle]백업및 복구

오라클 덤프 백업및 복구

윈도우 서버 기반 간단 정리

--디렉터리 조회

sqlplus 또는 dbtool 입력시작

SELECT * FROM DBA_DIRECTORIES ;

--D:\DEV\DATABASE\ORACLE21C\ADMIN\XE\DPDUMP 경로에 작업할 dpump_dir1 디렉토리 생성 

CREATE DIRECTORY dpump_dir1 AS 'D:\DEV\DATABASE\ORACLE21C\ADMIN\XE\DPDUMP';

--dpump_dir1 디렉토리레 권한을 system 계정에 부여

GRANT READ, WRITE ON DIRECTORY dpump_dir1 TO system;

--시스템 계정의 비밀번호가 없다면 생성

ALTER USER system IDENTIFIED BY 1234;

sqlplus 또는 dbtool 입력 끝


cmd 입력 시작

-- cmd 창을 열어 dmp 생성

expdp c##nerobong2/1234@xe SCHEMAS=C##NEROBONG2 DIRECTORY=dpump_dir1 DUMPFILE=schema_backup2.dmp LOGFILE=schema_backup.log

--전체복원

impdp c##nerobong2/1234@xe FULL=Y DIRECTORY=dpump_dir1 DUMPFILE=schema_backup2.dmp LOGFILE=full_restore.log

--스키마 복원

impdp c##nerobong2/1234@xe SCHEMAS=C##NEROBONG2 DIRECTORY=dpump_dir1 DUMPFILE=schema_backup2.dmp LOGFILE=schema_restore.log

--테이블 복원
impdp c##nerobong2/1234@xe TABLES=C##NEROBONG2.SALE DIRECTORY=dpump_dir1 DUMPFILE=schema_backup2.dmp LOGFILE=table_restore.log


cmd 입력 끝

2024년 7월 30일 화요일

[lunux]리눅스 폴더별 용량 확인

리눅스 폴더별 용량 확인

조회 하고자 하는 디렉토리 리스트가있는 경로로 이동후

du -h --max-depth=1

[oracle]옵티마이저(Optimizer)와 실행 계획, 실전 튜닝 기초 완벽 정리

[Oracle SQL] 옵티마이저(Optimizer)와 실행 계획, 실전 SQL 튜닝 가이드 이 가이드에서는 오라클 데이터베이스의 핵심 엔진인 옵티마이저의 원리를 이해하고, DBeaver와 SQL Developer를 활용하여 실제 쿼리의 실행 계획을...