Spring Security는 스프링 프레임워크에서 제공하는 강력하고 유연한 보안 프레임워크로, 애플리케이션의 인증(Authentication)과 권한 부여(Authorization)를 담당한다. 즉, 애플리케이션에 접근하는 사용자를 확인하고, 해당 사용자가 특정 기능이나 리소스에 접근할 수 있는 권한이 있는지를 결정하는 역할을 한다.

주요 개념

  1. 인증(Authentication):
    • 사용자가 누구인지 확인하는 과정이다. 예를 들어, 사용자가 아이디와 비밀번호를 입력해 자신의 신원을 증명하는 것을 의미한다.
    • Spring Security는 여러 인증 방법을 지원하며, 가장 일반적인 방식으로는 폼 기반 로그인, OAuth, JWT 등을 통한 인증이 있다.
  2. 권한 부여(Authorization):
    • 인증된 사용자가 애플리케이션 내에서 어떤 자원에 접근할 수 있는지 결정하는 과정이다. 예를 들어, 관리자만 특정 페이지에 접근할 수 있도록 설정하는 것이 권한 부여에 해당한다.
  3. 필터 체인(Filter Chain):
    • Spring Security의 핵심 구조 중 하나로, 여러 개의 보안 필터들이 체인 형태로 연결되어 있다. 사용자의 요청이 들어오면 이 필터 체인을 통과하며 인증과 권한 부여가 처리된다.
  4. SecurityContext:
    • 인증된 사용자의 정보를 담고 있는 컨텍스트로, 애플리케이션 전반에서 사용자의 인증 정보를 추적할 수 있게 한다.
  5. UserDetailsService:
    • 사용자 정보를 가져오는 인터페이스로, 데이터베이스 등에서 사용자 정보를 로드하여 인증을 수행할 때 사용된다.
  6. PasswordEncoder:
    • 비밀번호를 해싱하거나 검증하는 기능을 제공하는 인터페이스다. Spring Security는 기본적으로 여러 가지 암호화 알고리즘을 지원한다.

Spring Security 설정

Spring Security는 설정 방법이 유연하다. XML 설정, 어노테이션 설정, 그리고 Java Config를 통한 설정을 지원한다. 최근에는 Java Config를 사용하는 방식이 주로 사용한다.

예시)

 

@Configuration 
@EnableWebSecurity 
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Override 
    protected void configure(HttpSecurity http) throws Exception { 
    	http 
        .authorizeRequests() 
        .antMatchers("/admin/**").hasRole("ADMIN") 
        .antMatchers("/user/**").hasRole("USER") 
        .antMatchers("/", "/home").permitAll() 
        .and() 
        .formLogin() 
        .loginPage("/login") 
        .permitAll() 
        .and() 
        .logout() 
        .permitAll(); 
    } 
}​

위의 예시에서는 특정 URL 경로에 따라 접근 권한을 설정하고, 로그인 페이지와 로그아웃 기능을 설정하는 예시를 보여준다.

결론

Spring Security는 애플리케이션에 대한 인증과 권한 부여를 간단하면서도 유연하게 설정할 수 있게 해준다. 다양한 인증 방식을 지원하며, 강력한 보안 기능을 통해 애플리케이션을 안전하게 보호할 수 있다.

시간 복잡도(Time Complexity)는 알고리즘이 문제를 해결하는 데 걸리는 시간을 입력의 크기에 따라 측정하는 척도이다. 시간 복잡도는 알고리즘이 수행하는 연산의 수가 입력 크기에 따라 어떻게 변화하는지를 분석하여, 입력 크기가 커질 때 실행 시간이 어떻게 증가하는지를 설명한다.

#시간 복잡도의 주요 개념

  1. 상수 시간 복잡도 (O(1)): 알고리즘의 실행 시간이 입력 크기와 관계없이 일정하다. 예를 들어, 배열의 특정 인덱스에 접근하는 연산은 상수 시간 복잡도를 가진다.
  2. 로그 시간 복잡도 (O(log n)): 알고리즘의 실행 시간이 입력 크기의 로그에 비례하다. 예를 들어, 이진 탐색 알고리즘은 로그 시간 복잡도를 가진다.
  3. 선형 시간 복잡도 (O(n)): 알고리즘의 실행 시간이 입력 크기에 비례하다. 예를 들어, 배열의 모든 요소를 순회하는 연산은 선형 시간 복잡도를 가진다.
  4. 선형 로그 시간 복잡도 (O(n log n)): 알고리즘의 실행 시간이 입력 크기와 입력 크기의 로그의 곱에 비례하다. 예를 들어, 병합 정렬과 퀵 정렬 같은 효율적인 정렬 알고리즘은 선형 로그 시간 복잡도를 가진다.
  5. 이차 시간 복잡도 (O(n^2)): 알고리즘의 실행 시간이 입력 크기의 제곱에 비례하다. 예를 들어, 버블 정렬과 같은 단순 정렬 알고리즘은 이차 시간 복잡도를 가진다.
  6. 삼차 시간 복잡도 (O(n^3)): 알고리즘의 실행 시간이 입력 크기의 세제곱에 비례하다. 주로 삼중 루프를 사용하는 알고리즘에서 나타난다.
  7. 지수 시간 복잡도 (O(2^n)): 알고리즘의 실행 시간이 입력 크기의 지수 함수에 비례하다. 예를 들어, 완전 탐색 문제에서 지수 시간 복잡도가 나타날 수 있다.
  8. 팩토리얼 시간 복잡도 (O(n!)): 알고리즘의 실행 시간이 입력 크기의 팩토리얼 함수에 비례하다. 주로 순열을 생성하거나 검토하는 문제에서 나타난다.

1. O(1) - 상수 시간 복잡도

예시: 배열의 특정 인덱스에 접근하기

public class Main {
    public static int getElement(int[] arr, int index) {
        return arr[index];
    }

    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};
        int index = 2;
        System.out.println(getElement(arr, index));  // Output: 3
    }
}

위의 getElement 메서드는 배열의 특정 인덱스에 있는 요소를 반환한다. 배열의 크기와 상관없이 항상 일정한 시간 내에 완료된다.

2. O(n) - 선형 시간 복잡도

예시: 배열의 모든 요소를 출력하기

public class Main {
    public static void printElements(int[] arr) {
        for (int element : arr) {
            System.out.println(element);
        }
    }

    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};
        printElements(arr);
    }
}

printElements 메서드는 배열의 모든 요소를 한 번씩 출력한다. 배열의 크기가 두 배가 되면 출력하는 데 걸리는 시간도 두 배가 된다.

3. O(n^2) - 이차 시간 복잡도

예시: 버블 정렬

public class Main {
    public static void bubbleSort(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n - 1; i++) {
            for (int j = 0; j < n - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    // Swap arr[j] and arr[j + 1]
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
    }

    public static void main(String[] args) {
        int[] arr = {64, 34, 25, 12, 22, 11, 90};
        bubbleSort(arr);
        for (int num : arr) {
            System.out.print(num + " ");
        }
    }
}

버블 정렬 알고리즘은 두 개의 중첩된 반복문을 사용하여 배열을 정렬한다. 배열의 크기가 두 배가 되면 작업의 수는 약 네 배가 된다.

4. O(log n) - 로그 시간 복잡도

예시: 이진 탐색

public class Main {
    public static int binarySearch(int[] arr, int target) {
        int left = 0;
        int right = arr.length - 1;

        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (arr[mid] == target) {
                return mid;
            } else if (arr[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return -1; // Target not found
    }

    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        int target = 7;
        int result = binarySearch(arr, target);
        System.out.println(result); // Output: 6
    }
}

이진 탐색 알고리즘은 정렬된 배열에서 목표 값을 찾기 위해 배열을 절반씩 나누어 가며 검색한다. 배열의 크기가 두 배가 되면 비교 횟수는 한 번 증가한다.

5. O(n log n) - 선형 로그 시간 복잡도

예시: 합병 정렬

public class Main {
    public static void mergeSort(int[] arr) {
        if (arr.length < 2) {
            return;
        }
        int mid = arr.length / 2;
        int[] left = new int[mid];
        int[] right = new int[arr.length - mid];

        System.arraycopy(arr, 0, left, 0, mid);
        System.arraycopy(arr, mid, right, 0, arr.length - mid);

        mergeSort(left);
        mergeSort(right);
        merge(arr, left, right);
    }

    private static void merge(int[] arr, int[] left, int[] right) {
        int i = 0, j = 0, k = 0;
        while (i < left.length && j < right.length) {
            if (left[i] <= right[j]) {
                arr[k++] = left[i++];
            } else {
                arr[k++] = right[j++];
            }
        }
        while (i < left.length) {
            arr[k++] = left[i++];
        }
        while (j < right.length) {
            arr[k++] = right[j++];
        }
    }

    public static void main(String[] args) {
        int[] arr = {38, 27, 43, 3, 9, 82, 10};
        mergeSort(arr);
        for (int num : arr) {
            System.out.print(num + " ");
        }
    }
}

합병 정렬 알고리즘은 배열을 재귀적으로 나누고, 각 부분을 정렬하여 합치는 방식으로 작동한다. 이 과정에서 배열의 크기와 로그의 곱만큼 시간이 소요된다.

 

이 예시들은 Java에서 시간 복잡도를 어떻게 표현할 수 있는지를 보여준다. 각 알고리즘의 시간 복잡도는 문제의 입력 크기와 알고리즘의 실행 시간 간의 관계를 나타낸다.

1. 파이썬의 특징

 

  • 타입 시스템: 파이썬은 동적 타입 언어로 변수의 타입은 런타임에 결정되며, 변수에 다른 타입의 값을 할당할 수 있다.
  • 형변환: 파이썬은 암시적 형변환과 명시적 형변환을 모두 지원한다. 예를 들어, int와 float 간의 연산은 자동으로 float으로 변환되지만, 다른 타입 간의 변환은 명시적으로 해야 한다.
  • 이유: 파이썬은 동적 타입의 유연성을 제공하면서도, 사용자가 명시적으로 형변환을 해야 하는 경우를 통해 코드의 명확성을 유지하고자 한다.
# 자동 형변환 예시
int_value = 10
float_value = 5.5

result_add = int_value + float_value
print("덧셈 결과:", result_add)  # 출력: 15.5

 

# 명시적 형변환 예시
string_value = "100" #문자열
number_value = 50 #숫자

# 계산시 타입 명시 필요
result_subtract = int(string_value) - number_value
print("뺄셈 결과:", result_subtract)  # 출력: 50

+ 추가 의문점

그렇다면 파이썬은 동적인 언어임에도 데이터와 AI에 강점이 있는 이유는 무엇일까? 자바와 비교했을때 어떤 차이가 있는 것일까?

1. 자바의 서버 측 프로그래밍에서의 강점:

  • 성능: 자바는 컴파일된 바이트코드로 실행되며, JVM(Java Virtual Machine)이 최적화와 JIT(Just-In-Time) 컴파일을 통해 높은 성능을 제공한다. 이는 고성능 서버 애플리케이션을 작성하는 데 유리하다.
  • 스케일링: 자바는 대규모 애플리케이션과 서버 환경에서 높은 안정성과 확장성을 제공한다. 분산 시스템과 멀티스레딩을 지원하는 강력한 라이브러리와 프레임워크(예: Spring, Hibernate 등)가 많다.
  • 기술 스택: 자바는 오랜 역사를 가지고 있으며, 엔터프라이즈 환경에서 널리 사용된다. 따라서, 많은 기업들이 자바 기반의 기술 스택을 사용하고 있다.
  • 형상 관리: 정적 타입 언어로서 컴파일 타임에 오류를 잡을 수 있어, 코드의 안정성을 높이고 유지보수성을 향상시킬 수 있다.

2. 파이썬의 AI 및 데이터 과학에서의 강점:

  • 생산성: 파이썬은 문법이 간결하고 읽기 쉬워 코드 작성과 유지보수가 용이하다. 이는 빠른 프로토타입 개발과 실험에 유리하다.
  • 라이브러리와 프레임워크: AI와 데이터 과학 분야에서 강력한 생태계를 가지고 있다. 예를 들어, TensorFlow, PyTorch, scikit-learn, Pandas, NumPy 등과 같은 라이브러리가 있어, 모델 개발과 데이터 처리에 매우 유용하다.
  • 유연성: 동적 타입 언어로서 다양한 데이터 구조와 형식의 변화를 유연하게 처리할 수 있다. 이는 데이터 분석과 머신러닝 모델링에 유리하다.
  • 커뮤니티와 지원: 파이썬은 AI와 데이터 과학 분야에서 활발한 커뮤니티와 많은 자원(예: 튜토리얼, 학습 자료)이 있으며, 최신 연구 결과와 기술을 빠르게 적용할 수 있다.

● 정리

  • 자바: 안정성, 성능, 대규모 시스템 구축에 유리하며, 엔터프라이즈 환경에서 널리 사용된다. 그리고 강한 타입 시스템과 JVM의 최적화는 서버 애플리케이션에서 높은 신뢰성과 확장성을 보장한다.
  • 파이썬: 개발 속도와 유연성, 강력한 데이터 과학과 AI 생태계를 제공하며, 빠르게 실험하고 프로토타입을 개발할 수 있는 장점이 있다. 간결한 문법과 다양한 라이브러리는 데이터 분석과 머신러닝에 최적화되어 있다.

0. 의문점

자바스크립트 기초를 공부하면서 들었던 의문이 자바스크립트는 변수를 사용할때 대부분 타입에 상관없이 let으로 시작하고 변수를 적는 방법에서 약간씩 차이를 두는것이 특징이었다.

//예시

let num = 100; //숫자형

let str = "100"; //문자형

let boolean = true; //불린형

그리고 문자형 더하기는 뒤에 붙이는 형식이지만, 빼기, 곱하기, 나누기는 자동 형변환이 되는 것이었다. 그래서 '왜 자바는 형변환에 대해서 수동적이고, 자바스크립트는 자동으로 바꿔주는 것일까'라는 의문이 들었고, 이에 대해 알아보았다.

 

1. 언어의 설계 철학 차이

  • 자바(Java): 자바는 정적 타입 언어다. 즉, 변수의 타입이 명확하게 정해져 있으며, 컴파일 시점에 타입이 결정된다. 정적 타입 언어에서는 타입 안정성이 중요하다. 타입 안정성은 변수가 예기치 않은 타입으로 변경되어 발생할 수 있는 오류를 방지하는 것이다. 그렇기에 자바는 이러한 안전성을 보장하기 위해 자동 형변환을 최소화하며, 개발자가 명시적으로 형변환을 수행하도록 요구하는 것이다.
  • 자바스크립트(JavaScript): 자바스크립트는 동적 타입 언어다. 변수의 타입이 런타임에 결정되며, 변수의 타입을 자유롭게 변경할 수 있다. 이로 인해 자바스크립트는 코드 작성의 유연성이 크지만, 때로는 예기치 않은 타입 변환으로 인해 버그가 발생할 수 있다. 자바스크립트는 이러한 동적 특성 때문에 자동 형변환(암묵적 형변환)이 자주 발생하며, 개발자는 형변환에 대한 명시적 처리를 상대적으로 덜하게 된다.

2. 타입 안정성과 오류 방지

  • 자바(Java):  컴파일 단계에서 가능한 많은 오류를 잡아내고, 런타임 오류를 줄이기 위해 타입 체크를 엄격히 한다. 예를 들어, 문자열 "100"을 숫자로 나누는 경우, 자바에서는 컴파일러가 타입 불일치 오류를 발생시켜 이를 방지한다. 이는 개발자가 의도하지 않은 타입 변환으로 인한 오류를 피할 수 있도록 도와준다.
  • 자바스크립트(JavaScript): 자동 형변환을 통해 개발자가 편리하게 코드를 작성할 수 있도록 하지만, 때때로 예기치 않은 결과를 초래한다. 예를 들어, "100" / 2는 50이라는 결과를 반환하지만, "100" + 2는 "1002"라는 문자열이 된다. 이는 자바스크립트의 동적 타입 특성 때문에 발생하는 예시이다.

3. 언어 활용에 따른 특성

  • 자바(Java):  명시적인 형변환을 요구하는 것은 코드의 명확성유지보수성을 높이는 데 도움이 된다. 어떤 타입의 데이터가 어떻게 처리되는지 명확하게 드러나기 때문에, 코드를 읽는 사람에게 코드의 의도를 명확히 전달할 수 있다. 이는 특히 대규모 프로젝트에서 중요한데, 자동 형변환에 의존하면 예측하기 어려운 버그가 발생할 가능성이 커진다.
  • 자바스크립트(JavaScript): 유연성 생산성을 중요시하는 언어로, 빠르게 변화하는 환경에서 효율적인 개발을 가능하게 한다. 자바스크립트의 유연성 덕분에 개발자는 다양한 상황에 맞게 코드를 작성할 수 있고, 이를 통해 짧은 시간 내에 프로토타입을 제작하거나, 웹 애플리케이션을 구축할 수 있다.

결론

자바는 안정성과 명확성을 중시하는 정적 타입 언어로서, 자동 형변환을 최소화하고 명시적 형변환을 요구한다. 반면, 자바스크립트는 유연성을 중시하는 동적 타입 언어로서, 자동 형변환을 통해 개발자에게 편의를 제공하지만, 그로 인해 발생할 수 있는 예기치 않은 오류를 감수해야 한다.

 

'의문점' 카테고리의 다른 글

언어마다 형변환이 다른 이유 2(파이썬)  (0) 2024.08.17

+ Recent posts