backend
  • README
  • DOCS
    • Java Docs
    • Servlet Docs
    • JSP Docs
    • DB & SQL Docs
    • Spring Boot Docs
    • Spring Security Docs
    • AWS Docs
  • 설치하기
    • Intellij 설정
  • 자바
    • 01 Java란?
    • 02 자바 시작하기
    • 03 자료형과 연산자
    • 04 제어문
    • 05 메소드
    • 06 클래스 기초
      • Static 보충자료
      • 패키지 보충자료
    • 07 객체지향 프로그래밍
    • 08 클래스 더 알아보기
      • 열거형 ENUM 보충자료
    • 09 클래스와 자료형
      • 다형성 보충자료
      • 제네릭 보충자료
    • 10 컬렉션 프레임워크
      • 컬렉션 프레임워크 보충자료
    • 11 람다식과 함수형 프로그래밍
      • 람다식 보충자료
    • 12 오류 대비하기
      • 오류 보충자료
    • 13 멀티태스킹
      • 멀티태스킹 보충자료
    • 교재보충
      • java.lang
  • 스프링
    • 서블릿, JSP
      • 05 Servlet(서블릿)
        • 서블릿 보충자료
        • 서블릿 추가코드
        • XML, YAML, JSON
      • 06 JSP(자바 서버 페이지)
        • JSP 보충자료
      • 07 JSTL(JSP 스탠다드 태그 라이브러리)
        • JSTL 보충자료
      • 08 Cookie(쿠키), Session(세션)
      • 09 서블릿,필터,리스너
        • 서블릿,필터,리스너 보충자료
      • 11 도서관리 프로젝트 실습
    • Spring Boot
      • 01 스프링 등장 배경, 객체지향
        • 스프링 등장 배경, 객체지향 보충자료
      • 02 IOC(제어의 역전), DI(의존성 주입)
        • IOC 보충자료
        • DI 보충자료
      • 03 스프링 구조
        • 스프링 구조 보충설명
      • 04 테스트코드 실습
      • 05 스프링 빈 설정
        • 스프링 빈 설정 보충자료
      • 06 싱글톤
        • 싱글톤 보충 자료
      • 07 스프링 빈 자동설정
        • 스프링 빈 자동설정 보충자료
      • 08 빈 생명주기
        • 빈 생명주기 보충자료
      • 09 빈 스코프
        • 빈 스코프 보충자료
      • 10 스프링 MVC
        • 스프링 MVC 보충자료
        • 데이터베이스 연동에 필요한 부분
      • 11 Validation(검증)
        • Validation(검증) 보충자료
      • 12 Bean Validation(빈검증)
        • Bean Validation(빈검증) 보충자료
      • 13 예외처리
        • 예외처리 보충자료
      • 14 타입변환
      • 15 JDBC(Java Database Connectivity)
      • 16 커넥션풀
      • 17 트랜잭션
        • 트랜잭션 보충자료
      • 18 JDBC 템플릿 활용
      • 19 MyBatis
      • 20 JPA(Java Persistence API)
      • 22 게시판 프로젝트 실습
    • Spring Security
      • 보안(Security)
      • Spring Security
      • 2. Spring Security 알아보기
        • 보안 위협 실제 사례와 방어 전략
      • 3. Spring Security 기본 동작 흐름
      • 4. Spring Security로 인증 권한 추가하기
        • Spring Security의 인증 및 인가
      • 5. Spring Security에서 세션 관리하기
        • 세션(Session)과 쿠키(Cookie) 비교, 토큰(Token)과의 관계
        • 해싱 및 해싱알고리즘
        • base64
      • 6. Spring Security 악용 보호
        • SameSite
      • 7. Spring Security로 인가 권한 추가하기
      • 8. Bcrypt(비크립트) 암호화
      • OAuth2 적용하기
  • 네트워크
    • HTTP
    • OSI 7계층
  • DB&SQL
    • 01 Database(데이터베이스)와 SQL 개요
    • 02 관계형 모델
    • 03 집합
    • 04 JOIN 연산
    • 05 MySQL
      • 세이브포인트
      • DBeaver, Mysql 오토커밋 설정 관련
    • 06 SQL 기초
      • 예시데이터 쿼리문
    • 07 SQL 실습
      • 실습 스키마
    • 08 Join 활용
      • 실습스키마
    • 09 SQL 활용
      • 실습스키마
    • 10 정규화
      • 실습 스키마
    • 데이터타입
    • 예시 프로젝트 스키마 구성
  • AWS
    • SSL 연결하기
    • 보충설명
Powered by GitBook
On this page
  • JSP 관련 라이브러리
  • JSP의 동작 과정
  • 레이어드 아키텍처(Layered Architecture)
  • Static Import란?
  • ConcurrentHashMap: 개요
  • Synchronized 키워드 정리
  • 즉시 평가(Eager Evaluation)와의 차이
  • webapp 디렉터리란?
  • /WEB-INF 디렉터리의 역할
  1. 스프링
  2. 서블릿, JSP
  3. 06 JSP(자바 서버 페이지)

JSP 보충자료

JSP 관련 라이브러리

1. tomcat-embed-jasper

implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
  • 용도:

    • JSP 파일을 처리하기 위한 JSP 컴파일러입니다.

    • Spring Boot는 기본적으로 내장 Tomcat을 사용하며, JSP를 구동하려면 이 의존성을 추가해야 합니다.

  • 기능:

    • JSP 파일을 .java 파일로 컴파일하고 이를 서블릿으로 변환합니다.

    • JSP 실행 엔진 역할을 합니다.

2. jakarta.servlet.jsp.jstl-api

implementation 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api'
  • 용도:

    • JSTL(JavaServer Pages Standard Tag Library)의 API를 제공합니다.

  • 기능:

    • JSP 페이지에서 JSTL 태그(예: <c:forEach>, <c:if>)를 사용할 수 있도록 지원합니다.

3. org.glassfish.web:jakarta.servlet.jsp.jstl

implementation 'org.glassfish.web:jakarta.servlet.jsp.jstl'
  • 용도:

    • JSTL의 실제 구현체입니다.

  • 기능:

    • API에서 제공하는 태그를 실행하기 위한 구현체로, JSTL 태그를 JSP 페이지에서 사용할 때 필요한 라이브러리입니다.

4. jakarta.servlet-api

implementation 'jakarta.servlet:jakarta.servlet-api'
  • 용도:

    • 서블릿 API를 제공합니다.

  • 기능:

    • JSP는 서블릿 위에서 동작하기 때문에 서블릿 API가 필수적입니다.

    • JSP와 서블릿의 상호작용을 처리합니다.

JSP의 동작 과정

1. JSP 요청 처리 흐름

  1. 클라이언트 요청:

    • 클라이언트가 브라우저에서 JSP 파일(예: example.jsp)에 대한 요청을 보냅니다.

    • 예: http://localhost:8080/example.jsp.

  2. JSP 파일 처리 (서버):

    • 웹 컨테이너(Tomcat 등)가 JSP 파일을 처리하기 위해 다음 단계를 수행합니다:

      1. JSP 파일을 서블릿 자바 코드로 변환.

      2. 변환된 자바 코드를 컴파일하여 .class 파일 생성.

      3. 생성된 클래스 파일을 로드하고 인스턴스화하여 실행.

  3. 서블릿 실행:

    • 변환된 서블릿이 클라이언트 요청을 처리하고 동적인 HTML 또는 데이터를 생성.

  4. 응답 반환:

    • 생성된 HTML이 클라이언트(브라우저)로 전송되어 웹 페이지로 렌더링됩니다.


JSP의 내부 동작: 서블릿과의 연관

1. JSP가 서블릿으로 변환

  • JSP 파일은 실제로 HttpServlet 클래스를 상속받는 서블릿 코드로 변환됩니다.

  • JSP 파일의 각 부분은 서블릿 코드로 매핑됩니다:

    • HTML: out.print() 메서드로 변환.

    • JSP 스크립트릿(Java 코드): 서블릿의 메서드 내부에 포함.

예제 JSP 파일 (example.jsp):

<html>
<body>
    <h1>Welcome!</h1>
    <%
        String username = "John";
        out.println("Hello, " + username + "!");
    %>
</body>
</html>

변환된 서블릿 코드 (example_jsp.java):

public final class example_jsp extends HttpServlet {
    public void _jspService(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        response.setContentType("text/html");
        JspWriter out = response.getWriter();
        try {
            out.write("<html><body>");
            out.write("<h1>Welcome!</h1>");
            String username = "John";
            out.println("Hello, " + username + "!");
            out.write("</body></html>");
        } catch (Throwable t) {
            throw new ServletException(t);
        }
    }
}

2. 서블릿의 생명주기와 JSP

JSP가 서블릿으로 변환되면, 해당 서블릿은 일반 서블릿처럼 생명주기를 가집니다:

  1. 초기화 (init() 메서드):

    • JSP 서블릿이 메모리에 로드되며 한 번만 실행됩니다.

  2. 서비스 (service() 또는 _jspService()):

    • 클라이언트 요청을 처리하며, HTTP 요청(GET, POST 등)에 따라 동작.

  3. 종료 (destroy()):

    • JSP 서블릿이 메모리에서 해제될 때 호출됩니다.


JSP와 서블릿의 비교

특징

JSP

서블릿

코드 작성 방식

HTML에 Java 코드를 삽입

순수 Java 코드로 HTML을 생성

목적

HTML 중심 페이지에 동적인 Java 코드를 추가

Java 중심으로 HTML 페이지를 생성

변환 과정

JSP → 서블릿으로 변환 후 실행

직접 작성한 Java 클래스 실행

가독성

HTML 중심이므로 비개발자도 수정 가능

Java 중심이므로 개발자 친화적


JSP 동작 과정 상세

  1. JSP 파일 요청:

    • 클라이언트가 example.jsp를 요청.

  2. JSP 파일이 서블릿으로 변환:

    • 웹 컨테이너는 JSP를 Java 서블릿 코드로 변환합니다.

    • 변환된 서블릿 파일은 /work 디렉토리 같은 임시 폴더에 저장됩니다.

      • 예: apache-tomcat/work/Catalina/localhost/yourapp/org/apache/jsp/example_jsp.java.

  3. 컴파일:

    • 변환된 서블릿 파일(example_jsp.java)이 컴파일되어 .class 파일이 생성됩니다.

  4. 서블릿 실행:

    • 생성된 서블릿이 요청을 처리하고 HTML 응답을 생성합니다.

  5. 결과 반환:

    • 서버는 응답 데이터를 클라이언트에게 전송하여 웹 페이지를 표시합니다.

레이어드 아키텍처(Layered Architecture)

레이어드 아키텍처(Layered Architecture)란?

레이어드 아키텍처는 애플리케이션을 책임과 역할에 따라 여러 계층으로 나누어 설계하는 방법입니다. 각 계층은 독립적으로 동작하며, 서로 명확하게 정의된 인터페이스를 통해 통신합니다.


주요 레이어

  1. Domain Layer (도메인 계층):

    • 애플리케이션의 핵심 비즈니스 로직과 도메인 모델을 정의.

    • Entity, Value Object, Aggregate Root 등의 객체가 포함.

    • 비즈니스 규칙, 상태 관리, 핵심 기능을 캡슐화.

  2. Repository Layer (저장소 계층):

    • 데이터베이스와 도메인 계층 간의 데이터 접근 로직을 관리.

    • CRUD 작업을 담당하며, 도메인 객체의 영속성을 처리.

  3. Service Layer (서비스 계층):

    • 도메인 계층과 애플리케이션 계층을 연결하는 중간 계층.

    • 비즈니스 로직과 트랜잭션 관리를 처리하며, 도메인 계층의 여러 기능을 조합.

  4. Presentation Layer (프레젠테이션 계층):

    • 사용자 인터페이스와 관련된 모든 로직.

    • 사용자의 요청을 받아 Service Layer로 전달하고, 결과를 반환.

  5. Application Layer (애플리케이션 계층):

    • 도메인 로직을 실행하고 흐름을 관리.

    • 주로 애플리케이션의 유스케이스를 구현.


계층 나눔의 표현

  1. 레이어드 아키텍처 (Layered Architecture):

    • 애플리케이션을 논리적 계층으로 분리.

    • 예: Presentation Layer, Service Layer, Repository Layer, Domain Layer.

  2. DDD 용어:

    • Domain Layer:

      • 비즈니스 로직과 핵심 개념을 관리.

    • Infrastructure Layer:

      • 데이터베이스 및 외부 시스템과의 통신을 관리.

    • Application Layer:

      • 유스케이스를 구현하고 흐름을 관리.

  3. Hexagonal Architecture (Ports and Adapters):

    • 계층 아키텍처를 더 유연하게 표현한 구조.

    • Domain은 중심이고, Repository와 Service는 Port와 Adapter로 구분.


레이어드 아키텍처를 나누는 이유

  • 책임 분리(SRP): 각 레이어가 특정 역할에 집중.

  • 재사용성: 특정 계층의 로직을 다른 프로젝트에서도 재사용 가능.

  • 테스트 용이성: 각 계층을 독립적으로 테스트 가능.

  • 유지보수성: 계층 간 의존성이 낮아, 수정 시 다른 계층에 영향이 적음.

Static Import란?

Static Import는 Java에서 특정 클래스의 static 멤버(필드나 메서드)를 호출할 때 클래스 이름 없이 직접 사용할 수 있게 해주는 기능입니다. 일반적인 import와 달리 static 키워드를 사용하여 특정 클래스의 static 멤버를 가져옵니다.


1. Static Import의 문법

import static 패키지명.클래스명.필드명;
import static 패키지명.클래스명.메서드명;

또는, 클래스의 모든 static 멤버를 가져오려면 다음과 같이 사용합니다:

import static 패키지명.클래스명.*;

2. Static Import의 동작 방식

  • Static Import는 클래스의 static 멤버를 전역 변수 또는 전역 메서드처럼 사용할 수 있게 합니다.

  • 이를 통해 코드의 가독성을 높이고 반복적인 클래스 이름 입력을 줄일 수 있습니다.


3. Static Import의 사용 예제

예제 1: Math 클래스

Math 클래스의 static 메서드와 필드 사용 예제:

import static java.lang.Math.PI;
import static java.lang.Math.sqrt;

public class StaticImportExample {
    public static void main(String[] args) {
        double radius = 5;
        double area = PI * radius * radius;  // Math.PI 대신 PI 사용
        double root = sqrt(16);              // Math.sqrt 대신 sqrt 사용

        System.out.println("Area: " + area);
        System.out.println("Square Root: " + root);
    }
}
  • Math.PI → PI: 클래스 이름 없이 사용.

  • Math.sqrt → sqrt: 클래스 이름 없이 사용.


예제 2: Assertions (JUnit Test)

JUnit 테스트에서 static import를 사용해 간결한 코드를 작성:

import static org.junit.jupiter.api.Assertions.assertEquals;

class StaticImportTest {
    @Test
    void testAddition() {
        assertEquals(4, 2 + 2);  // Assertions.assertEquals 대신 간단히 assertEquals 사용
    }
}
  • Assertions.assertEquals → assertEquals: 간결하게 표현 가능.

ConcurrentHashMap: 개요

ConcurrentHashMap은 Java의 동시성 컬렉션 중 하나로, 멀티스레드 환경에서 안전하게 사용할 수 있는 Map 구현체입니다. 여러 스레드가 동시에 접근해도 데이터의 일관성과 안정성을 보장하면서도 높은 성능을 제공합니다.


1. 주요 특징

  1. 멀티스레드 안전(Thread-safe):

    • 내부적으로 세그먼트 락(Segment Lock) 또는 버킷 락킹 메커니즘을 사용하여 동기화를 제공합니다.

    • Map 전체를 잠그는 대신, 특정 버킷에만 락을 걸어 성능을 최적화.

  2. Non-blocking 읽기(Read):

    • 대부분의 읽기 연산(get)은 락을 사용하지 않고 비차단(non-blocking) 방식으로 수행됩니다.

  3. 성능 최적화:

    • 동기화된 Map(Collections.synchronizedMap())에 비해 더 높은 동시성을 제공합니다.

    • 내부적으로 CAS(Compare-And-Swap) 연산과 락 스트라이핑(Lock Stripping) 기법을 사용해 성능 향상.

  4. NULL 키와 값 허용 불가:

    • null 키와 null 값을 허용하지 않습니다.


2. 동작 원리

기존 Hashtable과의 차이점

  • Hashtable은 모든 메서드에서 Map 전체를 동기화(Synchronized)하여 동시성을 보장하지만, 성능이 낮습니다.

  • ConcurrentHashMap은 데이터를 여러 세그먼트로 나누고 필요한 세그먼트에만 락을 걸어 동시성 이슈를 해결하면서 성능을 유지합니다.

락 스트라이핑(Lock Stripping)

  • ConcurrentHashMap은 Map의 데이터를 여러 버킷(Segment)으로 분리합니다.

  • 각 버킷은 독립적으로 락이 걸리므로, 다른 스레드가 동시에 다른 버킷에 접근할 수 있습니다.

  • 예: 한 스레드가 key1을 업데이트하고 있을 때, 다른 스레드는 key2에 자유롭게 접근 가능.

CAS(Compare-And-Swap)

  • 쓰기 연산 중에도 일부는 CAS를 사용하여 락 없이 동작할 수 있습니다.

  • 예: 특정 버킷에서 값 삽입이나 교체 시 CAS로 값을 안전하게 업데이트.


3. 주요 메서드

1) 데이터 삽입

map.put(key, value);
  • 키와 값을 Map에 삽입. 필요 시 락을 걸어 동기화를 보장.

2) 데이터 읽기

map.get(key);
  • 락 없이 데이터를 읽음. 매우 빠른 비차단(non-blocking) 동작.

3) 데이터 삭제

map.remove(key);
  • 특정 키에 해당하는 데이터를 삭제.

4) 동시 업데이트

  • compute, merge, putIfAbsent 등의 메서드를 통해 동시 업데이트를 안전하게 수행.

map.compute(key, (k, v) -> (v == null) ? 1 : v + 1); // 값이 없으면 1, 있으면 1 증가

5) 데이터 탐색

map.forEach(1, (key, value) -> {
    System.out.println(key + ": " + value);
});
  • 병렬적으로 데이터를 탐색 가능. (파라미터는 병렬 스레드 수)


4. 사용해야 하는 이유

1) 멀티스레드 환경에서 안전한 Map 필요

  • 여러 스레드가 동시에 Map을 읽고 쓰는 상황에서 ConcurrentHashMap은 동기화 문제를 해결합니다.

  • 예: 웹 애플리케이션의 캐시, 상태 관리.

2) 성능 최적화

  • 기존의 Hashtable이나 synchronizedMap보다 더 높은 동시성과 성능 제공.

  • 읽기와 쓰기를 동시에 수행해야 하는 환경에서 효율적.

3) 데이터 무결성 유지

  • 여러 스레드가 값을 삽입하거나 업데이트하는 상황에서도 데이터의 무결성을 유지.


5. 사용 사례

1) 애플리케이션 캐시

  • 데이터를 읽는 작업이 많고 쓰는 작업이 적은 경우.

  • 예: 웹 애플리케이션에서 자주 조회되는 데이터를 캐싱.

ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();
cache.put("user1", "John");
cache.put("user2", "Jane");

String user = cache.get("user1"); // 빠른 읽기

2) 실시간 사용자 세션 관리

  • 채팅 애플리케이션 등에서 사용자의 세션 데이터를 저장.

ConcurrentHashMap<String, UserSession> sessions = new ConcurrentHashMap<>();
sessions.put(sessionId, new UserSession(userId));

3) 통계 데이터 수집

  • 다수의 스레드가 동시에 통계 데이터를 업데이트.

ConcurrentHashMap<String, Integer> stats = new ConcurrentHashMap<>();
stats.compute("hits", (key, value) -> (value == null) ? 1 : value + 1);

4) 동시성 제어

  • 여러 작업이 동시에 진행되는 환경에서 값 업데이트 및 제어.

map.putIfAbsent("key1", "value1"); // 키가 없으면 삽입
map.computeIfPresent("key1", (k, v) -> v + "_updated"); // 키가 있을 때 값 수정

6. 주의사항

  1. NULL 키와 값 허용 불가:

    • ConcurrentHashMap은 null 키나 값을 허용하지 않습니다.

    • 이유: 동시성 환경에서 null 처리는 혼란을 초래할 수 있기 때문.

  2. 전체 락 필요 작업:

    • size() 또는 containsValue() 같은 작업은 전체 Map을 순회하므로 성능이 저하될 수 있습니다.

  3. 일관성 제한:

    • 병렬 접근 중 일부 작업에서 최신 상태가 보장되지 않을 수 있습니다.


7. 예제 코드

멀티스레드 환경에서의 활용

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

        // 쓰레드 1: 값 추가
        Thread t1 = new Thread(() -> {
            map.put("A", 1);
            map.put("B", 2);
        });

        // 쓰레드 2: 값 업데이트
        Thread t2 = new Thread(() -> {
            map.compute("A", (key, value) -> (value == null) ? 1 : value + 1);
        });

        // 쓰레드 3: 읽기
        Thread t3 = new Thread(() -> {
            System.out.println("Value of A: " + map.get("A"));
        });

        t1.start();
        t2.start();
        t3.start();
    }
}

**8. ConcurrentHashMap vs synchronizedMap vs Hashtable

특징

ConcurrentHashMap

synchronizedMap

Hashtable

멀티스레드 안전

예

예

예

성능

높음

낮음 (Map 전체 락)

낮음 (Map 전체 락)

NULL 키/값 허용

허용하지 않음

허용 가능

허용하지 않음

부분 락 지원

예 (버킷 단위 락)

아니요 (전체 락)

아니요 (전체 락)

주요 사용 사례

고성능 멀티스레드 환경

간단한 동기화 필요

레거시 코드


결론

  • 왜 사용해야 하는가?

    • 멀티스레드 환경에서 Map을 동기화하며, 동시에 높은 성능을 제공하기 때문.

  • 사용처:

    • 실시간 데이터 처리, 캐싱, 통계 수집, 세션 관리 등 동시성 이슈가 발생하는 상황.

Synchronized 키워드 정리

**synchronized**는 멀티스레드 환경에서 **동기화(Synchronization)**를 제공하여 여러 스레드가 공유 자원에 동시에 접근할 때 발생하는 문제를 방지합니다. 이를 통해 데이터의 무결성과 일관성을 유지할 수 있습니다.


1. synchronized란?

  1. 임계영역 (Critical Section):

    • 여러 스레드가 공유 자원에 동시에 접근하지 못하도록 제한하는 코드 영역.

    • synchronized는 임계영역을 보호하여 한 번에 한 스레드만 실행 가능하게 합니다.

  2. 모니터 락 (Monitor Lock):

    • Java에서 모든 객체는 **모니터 락(Monitor Lock)**을 가지고 있습니다.

    • synchronized 키워드는 이 락을 이용해 스레드 접근을 제어합니다.

    • 스레드가 synchronized 영역에 진입하려면 해당 객체의 락을 획득해야 합니다.

  3. 락 획득 과정:

    • 스레드는 synchronized 메서드나 블록에 진입하려면 객체의 락을 획득.

    • 락이 이미 다른 스레드에 의해 사용 중이면 현재 스레드는 Blocked 상태로 대기.

    • 락을 해제하면 대기 중인 스레드 중 하나가 락을 획득하고 실행 가능.


2. synchronized의 장점과 단점

장점

  1. 데이터 무결성 유지:

    • 여러 스레드가 동시에 데이터를 수정할 때 발생하는 Race Condition(경합 상태) 문제 방지.

  2. 데이터 일관성 보장:

    • 스레드 간 데이터 일관성 유지.

단점

  1. 성능 저하:

    • 동기화로 인해 스레드가 대기 상태로 전환되므로 실행 속도가 느려질 수 있음.

  2. 무한 대기:

    • 특정 스레드가 락을 반환하지 않으면 다른 스레드가 무한히 대기 상태에 빠질 수 있음.

  3. 공정성 문제:

    • 락이 해제되었을 때 어떤 스레드가 락을 획득할지 예측할 수 없음.


3. synchronized의 종류와 사용법

(1) 메서드 동기화

  • synchronized 키워드를 메서드에 붙여 메서드 전체를 임계영역으로 설정.

  • 객체의 인스턴스 락을 사용.

예제 코드:

public class BankAccount {
    private int balance = 100;

    public synchronized void withdraw(int amount) {
        if (balance >= amount) {
            balance -= amount;
            System.out.println("Withdrawn: " + amount + ", Remaining Balance: " + balance);
        } else {
            System.out.println("Insufficient Balance.");
        }
    }
}
  • 특징:

    • 한 번에 하나의 스레드만 withdraw 메서드를 실행 가능.

    • 다른 스레드가 withdraw 호출 시 대기.


(2) 블록 동기화

  • synchronized 키워드를 특정 코드 블록에 적용.

  • 객체의 특정 부분만 보호하여 성능 최적화 가능.

예제 코드:

public class BankAccount {
    private int balance = 100;

    public void withdraw(int amount) {
        synchronized (this) {
            if (balance >= amount) {
                balance -= amount;
                System.out.println("Withdrawn: " + amount + ", Remaining Balance: " + balance);
            } else {
                System.out.println("Insufficient Balance.");
            }
        }
    }
}
  • 특징:

    • this 객체의 락만 필요한 부분에 걸어 성능 최적화.

    • 락의 범위를 최소화하여 불필요한 대기를 줄임.


(3) 클래스 동기화 (정적 메서드)

  • synchronized를 정적 메서드에 사용하면 클래스 수준의 락이 적용.

  • 클래스 락(Class Object Lock)을 사용.

예제 코드:

public class BankAccount {
    private static int totalBalance = 1000;

    public static synchronized void deposit(int amount) {
        totalBalance += amount;
        System.out.println("Deposited: " + amount + ", Total Balance: " + totalBalance);
    }
}
  • 특징:

    • 클래스 전체에서 공유되는 자원(totalBalance)에 대해 동기화.

    • 클래스 락을 사용하므로 인스턴스와는 독립적으로 동작.


4. synchronized를 사용한 문제 해결

(1) Race Condition (경합 상태) 방지

  • 여러 스레드가 동시에 공유 자원을 수정할 때 발생하는 문제.

  • synchronized를 사용해 문제 해결.

문제 코드:

public class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}
  • 위 코드에서 여러 스레드가 동시에 increment를 호출하면 count 값이 정확하지 않음.

해결 코드:

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

(2) 데이터 일관성 유지

문제 상황:

  • 은행 계좌에서 여러 스레드가 동시에 입출금을 처리하는 경우.

문제 코드:

public class BankAccount {
    private int balance = 100;

    public void withdraw(int amount) {
        if (balance >= amount) {
            balance -= amount;
        }
    }
}
  • 위 코드에서 두 스레드가 동시에 withdraw를 호출하면, 잔액이 음수가 될 수 있음.

해결 코드:

public class BankAccount {
    private int balance = 100;

    public synchronized void withdraw(int amount) {
        if (balance >= amount) {
            balance -= amount;
        }
    }
}

5. 주의사항

  1. 지역 변수는 동기화 대상이 아님:

    • 지역 변수는 스레드마다 별도로 할당되므로 동기화 걱정이 필요 없음.

    • 공유 자원에만 동기화를 적용.

  2. final 변수는 동기화 불필요:

    • final 변수는 불변이므로 동기화할 필요 없음.

예제:

public void method() {
    final int localVariable = 10; // 동기화 필요 없음
}
  1. 임계영역 최소화:

    • 성능 최적화를 위해 동기화 범위를 최소화.

    • 불필요하게 synchronized를 남용하지 말 것.


6. Synchronized의 단점

  1. 무한 대기 (Deadlock):

    • 락을 반환하지 않는 상태가 지속되면 다른 스레드가 무한히 대기.

  2. 공정성 문제:

    • 여러 스레드가 락을 대기할 때 어떤 스레드가 락을 획득할지 알 수 없음.

    • 특정 스레드가 장시간 대기할 가능성 존재.

  3. 성능 저하:

    • 동기화는 성능에 영향을 미침. 특히, 불필요한 동기화는 병목현상을 초래.


7. 결론

  • synchronized는 멀티스레드 환경에서 데이터 무결성과 일관성을 보장하는 강력한 도구.

  • 사용 시 성능 최적화를 위해 임계영역 최소화와 동기화 범위 제한을 염두에 두어야 함.

  • 더 복잡한 동기화 요구사항이 있다면 ReentrantLock 또는 Concurrent Collections를 고려.


public class ConcurrentTest {

        static class SynchronizedExample {
            private int balance = 100;

            public void withdraw(int amount) {
                synchronized (this) {  // 임계영역 최소화
                    if (balance >= amount) {
                        balance -= amount;
                        System.out.println("Withdrawn: " + amount + ", Remaining Balance: " + balance);
                    } else {
                        System.out.println("Insufficient Balance.");
                    }
                }
            }

            public synchronized void deposit(int amount) {  // 메서드 전체 동기화
                balance += amount;
                System.out.println("Deposited: " + amount + ", New Balance: " + balance);
            }

            public synchronized int getBalance() {
                return balance;
            }
        }

    static class SynchronizedExampleWithoutSync {
        private int balance = 100;

        public void withdraw(int amount) {
            if (balance >= amount) {
                try {
                    // 인위적으로 스레드가 멈추도록 해서 동시성을 유발
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                balance -= amount;
                System.out.println("Withdrawn: " + amount + ", Remaining Balance: " + balance);
            } else {
                System.out.println("Insufficient Balance.");
            }
        }

        public void deposit(int amount) {
            balance += amount;
            System.out.println("Deposited: " + amount + ", New Balance: " + balance);
        }

        public int getBalance() {
            return balance;
        }
    }

    @Test
        public void testWithdrawAndDepositSingleThread() {
            SynchronizedExample account = new SynchronizedExample();

            // Test deposit
            account.deposit(50);
            assertEquals(150, account.getBalance());

            // Test withdraw
            account.withdraw(30);
            assertEquals(120, account.getBalance());

            // Test insufficient funds
            account.withdraw(200);  // Should not decrease balance
            assertEquals(120, account.getBalance());
        }

    @Test
    public void testSynchronizedWithdraw() throws InterruptedException {
        SynchronizedExample account = new SynchronizedExample();

        Thread t1 = new Thread(() -> account.withdraw(80));
        Thread t2 = new Thread(() -> account.withdraw(60));

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        // synchronized: 음수 잔액 발생하지 않음
        assertTrue(account.getBalance() >= 0, "Balance should not be negative");
    }

    @Test
    public void testNonSynchronizedWithdraw() throws InterruptedException {
        SynchronizedExampleWithoutSync account = new SynchronizedExampleWithoutSync();

        Thread t1 = new Thread(() -> account.withdraw(80));
        Thread t2 = new Thread(() -> account.withdraw(60));

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        // synchronized 없는 경우: 음수 잔액 발생 가능
        System.out.println("Final Balance: " + account.getBalance());
        assertTrue(account.getBalance() < 0, "Balance should be negative due to race condition");
    }
}

즉시 평가(Eager Evaluation)와의 차이

  • 즉시 평가 (Eager Evaluation):

    • 값이나 표현식을 즉시 계산.

    • 대부분의 프로그래밍 언어에서 기본적으로 사용하는 방식.

    • 계산 결과가 실제로 사용되지 않아도 계산이 실행됨.

    예제 (즉시 평가):

    int result = 5 * 5;  // 바로 계산되어 result에 25 저장
  • 지연 평가 (Lazy Evaluation):

    • 값이나 표현식을 필요할 때 계산.

    • 계산이 필요하지 않은 경우 실행되지 않음.

    • 메모리 효율과 실행 속도를 높일 수 있음.

    예제 (지연 평가):

    Supplier<Integer> result = () -> 5 * 5; // 계산을 미룸
    System.out.println(result.get());      // 이 시점에서 계산 수행

계산을 미룬다는 것의 의미

  1. 계산을 "나중으로" 미룬다:

    • 값을 즉시 계산하지 않고, 계산에 필요한 표현식(함수 또는 연산)을 기억해둠.

    • 이후 프로그램 실행 중 해당 값이 필요할 때 계산을 수행.

    예제:

    Supplier<Integer> lazyValue = () -> 10 + 20; // 계산 미룸
    System.out.println("값을 요청하기 전까지 계산하지 않음");
    System.out.println("결과: " + lazyValue.get()); // 여기서 계산 수행
  2. 필요하지 않으면 계산하지 않는다:

    • 특정 조건에 따라 값이 사용되지 않으면, 계산이 전혀 실행되지 않음.

    예제:

    Supplier<Integer> expensiveComputation = () -> {
        System.out.println("계산 수행 중...");
        return 10 + 20;
    };
    
    // 실제 값이 필요하지 않으면 계산 수행 안 됨
    if (false) {
        System.out.println(expensiveComputation.get()); // 실행되지 않음
    }

webapp 디렉터리란?

  1. Java 웹 애플리케이션의 루트 디렉터리:

    • src/main/webapp은 Maven 및 Gradle 프로젝트의 표준 디렉터리 구조에서 웹 애플리케이션 리소스를 배치하는 디렉터리입니다.

    • WAR(Web Application Archive) 파일 생성 시, webapp 디렉터리는 애플리케이션의 루트로 간주됩니다.

  2. 웹 리소스를 포함:

    • HTML, CSS, JavaScript, 이미지와 같은 정적 리소스와 JSP 파일을 포함합니다.

    • 서버에 배포될 때 이 디렉터리 구조가 그대로 유지됩니다.


webapp의 디렉터리 구조

webapp 디렉터리의 표준 구조는 다음과 같습니다:

src/main/webapp/
├── META-INF/
├── WEB-INF/
│   ├── web.xml
│   ├── views/
│   │   └── example.jsp
├── static/
│   ├── css/
│   │   └── styles.css
│   ├── js/
│   │   └── scripts.js
│   └── images/
│       └── logo.png
├── index.html

1. META-INF/

  • 역할:

    • Java 애플리케이션의 메타데이터 파일이 포함됩니다.

  • 내용:

    • 일반적으로 JAR 또는 WAR 파일의 설정 파일을 저장합니다.

    • MANIFEST.MF 파일 등이 포함될 수 있습니다.

2. WEB-INF/

  • 역할:

    • 외부에서 접근할 수 없는 보안 리소스를 저장합니다.

  • 내용:

    • web.xml: 애플리케이션의 배포 서술자(Deployment Descriptor) 파일.

    • views/: JSP 파일을 저장하는 디렉터리로 클라이언트가 직접 접근할 수 없습니다.

3. static/

  • 역할:

    • 정적 리소스를 저장합니다.

  • 내용:

    • CSS, JavaScript, 이미지 등의 정적 파일.

    • Spring Boot에서는 src/main/resources/static에 배치된 정적 리소스가 기본적으로 매핑됩니다.

4. HTML 파일

  • 역할:

    • 애플리케이션의 정적 HTML 파일이 포함됩니다.

    • index.html은 애플리케이션의 기본 페이지로 사용됩니다.


webapp 디렉터리의 특징

  1. 서버에서의 동작:

    • webapp 디렉터리는 애플리케이션이 서버에 배포될 때, ROOT 디렉터리로 변환됩니다.

    • 예: src/main/webapp/index.html → http://localhost:8080/index.html

  2. JSP 파일의 처리:

    • WEB-INF/views에 있는 JSP 파일은 직접 접근할 수 없으며, 반드시 서블릿이나 컨트롤러를 통해 호출해야 합니다.

  3. 보안:

    • WEB-INF에 저장된 파일들은 외부 클라이언트가 직접 접근할 수 없습니다.

    • 정적 리소스는 /static, /public, 또는 /resources 디렉터리 아래에 배치해 접근 가능합니다.

  4. WAR 파일 생성:

    • 프로젝트 빌드 시 webapp 디렉터리는 .war 파일로 패키징됩니다.

    • WAR 파일의 구조는 다음과 같습니다:

      arduino코드 복사example.war
      ├── META-INF/
      ├── WEB-INF/
      │   ├── web.xml
      │   ├── views/
      │   │   └── example.jsp
      ├── static/
      │   ├── css/
      │   ├── js/
      │   └── images/
      └── index.html

/WEB-INF 디렉터리의 역할

/WEB-INF 디렉터리는 Java 웹 애플리케이션의 구조에서 보안상 외부 클라이언트로부터 접근할 수 없는 리소스를 포함합니다. 이는 JSP, web.xml, 설정 파일 등이 위치하는 특별한 디렉터리로, 다음과 같은 특징이 있습니다.

1. 외부 접근 불가

  • /WEB-INF에 위치한 파일들은 클라이언트가 직접 URL로 접근할 수 없습니다.

    • 예: http://example.com/WEB-INF/views/example.jsp는 접근 불가.

  • 이는 보안상의 이유로 설계된 것이며, 내부적으로만 접근 가능하도록 보장합니다.

    • 예외: 서버 내부에서 RequestDispatcher를 통해 포워딩하여 사용할 수 있습니다.


web.xml의 역할

/WEB-INF/web.xml은 Java EE 표준 스펙에서 **배포 서술자(Deployment Descriptor)**로 사용됩니다.

주요 역할

  1. 서블릿 및 서블릿 매핑 정의

    • 특정 URL 요청이 어떤 서블릿으로 처리될지 설정합니다.

    • 예:

      <servlet>
          <servlet-name>exampleServlet</servlet-name>
          <servlet-class>com.example.ExampleServlet</servlet-class>
      </servlet>
      <servlet-mapping>
          <servlet-name>exampleServlet</servlet-name>
          <url-pattern>/example</url-pattern>
      </servlet-mapping>
  2. 필터 설정

    • 요청과 응답을 가로채거나 수정하는 필터를 설정합니다.

    • 예:

      <filter>
          <filter-name>exampleFilter</filter-name>
          <filter-class>com.example.ExampleFilter</filter-class>
      </filter>
      <filter-mapping>
          <filter-name>exampleFilter</filter-name>
          <url-pattern>/*</url-pattern>
      </filter-mapping>
  3. 리스너 설정

    • 애플리케이션의 생명주기를 관리하거나 초기화 로직을 정의하는 리스너를 등록합니다.

스프링 이후의 변화

  • web.xml의 역할은 대부분 스프링에서 어노테이션으로 대체되었습니다.

    • 예: 서블릿과 매핑 설정은 @WebServlet과 같은 어노테이션으로 처리.

    • DispatcherServlet도 자바 설정(@Configuration)으로 초기화 가능.


/WEB-INF/views

특징

  1. JSP 파일의 저장 위치

    • JSP 파일을 /WEB-INF/views 디렉터리에 배치하는 것이 관례입니다.

    • 이 디렉터리에 배치된 JSP 파일은 클라이언트가 직접 요청할 수 없으며, 반드시 컨트롤러를 통해 접근해야 합니다.

  2. 뷰 리졸버(View Resolver) 설정

    • 스프링 MVC에서는 뷰 리졸버를 통해 JSP 파일을 렌더링합니다.

    • 예: application.properties 설정

      spring.mvc.view.prefix=/WEB-INF/views/
      spring.mvc.view.suffix=.jsp
    • 위 설정에 따라 return "example";을 호출하면 /WEB-INF/views/example.jsp가 렌더링됩니다.

  3. 보안 강화

    • 클라이언트가 JSP 파일에 직접 접근할 수 없으므로, JSP 파일이 무분별하게 노출되는 것을 방지할 수 있습니다.

    • 컨트롤러를 통해서만 접근 가능하므로, 요청을 적절히 검증하고 처리할 수 있습니다.


동작 원리

  1. 클라이언트가 /example 요청을 보냅니다.

  2. DispatcherServlet이 해당 요청을 처리할 컨트롤러를 찾습니다.

  3. 컨트롤러에서 JSP 파일로 포워딩하거나 데이터를 전달합니다.

  4. JSP 파일은 /WEB-INF/views 내부에서 로드되고 렌더링된 HTML이 클라이언트에 반환됩니다.


장점

  1. 보안성:

    • JSP 파일이 외부로 노출되지 않아 무단 접근을 방지할 수 있습니다.

  2. 구조화:

    • 웹 애플리케이션의 리소스를 체계적으로 관리할 수 있습니다.

  3. 컨트롤러 중심 설계:

    • 모든 요청은 컨트롤러를 통해 처리되므로 로직 분리가 명확합니다.


요약

디렉터리/파일

설명

/WEB-INF

외부 접근이 불가능한 디렉터리로, JSP와 설정 파일들이 포함됨.

web.xml

서블릿 매핑, 필터 설정 등을 정의하는 배포 서술자.

/WEB-INF/views

JSP 파일이 위치하는 디렉터리. 클라이언트가 직접 접근할 수 없으며 컨트롤러를 통해 사용됨.

위 구조는 보안성과 유지보수성을 높이기 위한 표준 설계 방식입니다. Spring Boot와 같은 현대적인 프레임워크에서는 어노테이션과 Java Config를 통해 많은 부분을 대체하지만, 여전히 기본 원칙은 유지됩니다.

Previous06 JSP(자바 서버 페이지)Next07 JSTL(JSP 스탠다드 태그 라이브러리)

Last updated 5 months ago