반응형

동시성 문제를 해결하는 4가지 방법

안녕하세요, 오늘은 멀티스레드 환경에서 발생하는 동시성 문제를 해결하는 네 가지 방법에 대해 이야기해보려고 합니다. 동시성 문제는 여러 스레드가 동시에 같은 자원에 접근하면서 데이터 무결성이 깨질 수 있는 상황을 말하는데요. 이를 해결하기 위해 자바와 Spring Boot에서 사용할 수 있는 다양한 접근법을 예시와 함께 알아보겠습니다.

1. 자바 synchronized 키워드

가장 기본적인 동시성 제어 방법 중 하나는 자바의 synchronized 키워드를 사용하는 것입니다. 이 방식은 특정 코드 블록이나 메서드에 대해 한 번에 하나의 스레드만 접근할 수 있도록 잠금을 걸어줍니다.

언제 사용하나요?

  • 간단한 동기화가 필요한 경우
  • 객체 단위로 락을 걸어 데이터 무결성을 지키고 싶을 때

Spring Boot 예시

재고 관리 시스템에서 동시에 재고를 감소시키는 상황을 생각해봅시다.

@Service
public class StockService {
    private int stock = 100;

    public synchronized void decreaseStock(int quantity) {
        if (stock >= quantity) {
            stock -= quantity;
            System.out.println("재고 감소! 남은 재고: " + stock);
        } else {
            throw new RuntimeException("재고 부족!");
        }
    }
}
 

위 코드에서 synchronized를 사용하면 여러 스레드가 동시에 decreaseStock 메서드에 접근하더라도 순차적으로 실행됩니다. 단점은 성능이 중요한 대규모 시스템에서는 병목 현상이 발생할 수 있다는 점입니다.


2. 낙관적 락 (Optimistic Lock)

낙관적 락은 "충돌이 별로 없을 거야!"라는 낙관적인 가정을 기반으로 동시성을 제어합니다. 데이터를 수정할 때 충돌 여부를 버전 번호나 타임스탬프를 통해 체크하고, 충돌이 발생하면 예외를 던져서 처리합니다.

언제 사용하나요?

  • 충돌 가능성이 낮은 환경
  • 성능 최적화가 중요한 경우

Spring Boot 예시

JPA를 사용해 낙관적 락을 구현해보겠습니다. @Version 어노테이션을 활용하면 됩니다.

@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private int stock;

    @Version
    private int version; // 버전 필드 추가
}

@Service
public class ProductService {
    @Autowired
    private ProductRepository productRepository;

    @Transactional
    public void decreaseStock(Long productId, int quantity) {
        Product product = productRepository.findById(productId)
                .orElseThrow(() -> new RuntimeException("상품 없음"));
        if (product.getStock() >= quantity) {
            product.setStock(product.getStock() - quantity);
            productRepository.save(product);
        } else {
            throw new RuntimeException("재고 부족!");
        }
    }
}
 

위 코드에서 트랜잭션이 끝날 때 JPA가 버전을 체크합니다. 다른 스레드가 먼저 수정해서 버전이 달라지면 OptimisticLockException이 발생하고, 이를 통해 충돌을 감지합니다. 성능이 비관적 락보다 좋지만, 충돌이 자주 발생하면 롤백 처리가 늘어날 수 있어요.


3. 비관적 락 (Pessimistic Lock)

비관적 락은 "충돌이 자주 발생할 거야!"라는 비관적인 가정을 하고, 아예 트랜잭션 시작 시점에 데이터베이스에 락을 겁니다. 다른 스레드는 락이 해제될 때까지 기다려야 합니다.

언제 사용하나요?

  • 충돌 가능성이 높은 환경
  • 데이터 무결성이 매우 중요한 경우 (예: 금융 시스템)

Spring Boot 예시

JPA에서 @Lock 어노테이션을 사용해 비관적 락을 설정할 수 있습니다.

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT p FROM Product p WHERE p.id = :id")
    Optional<Product> findByIdWithPessimisticLock(@Param("id") Long id);
}

@Service
public class ProductService {
    @Autowired
    private ProductRepository productRepository;

    @Transactional
    public void decreaseStock(Long productId, int quantity) {
        Product product = productRepository.findByIdWithPessimisticLock(productId)
                .orElseThrow(() -> new RuntimeException("상품 없음"));
        if (product.getStock() >= quantity) {
            product.setStock(product.getStock() - quantity);
            productRepository.save(product);
        } else {
            throw new RuntimeException("재고 부족!");
        }
    }
}

 

PESSIMISTIC_WRITE 락을 걸면 해당 레코드는 트랜잭션이 끝날 때까지 다른 스레드가 수정할 수 없습니다. 성능은 낙관적 락보다 떨어지지만, 중요한 데이터의 정확성을 보장할 수 있어요.


4. 캐시 서버 도입

마지막으로, 캐시 서버(예: Redis)를 활용하는 방법입니다. 이건 돈이 좀 들 수 있는 솔루션이지만, 동시성 문제를 근본적으로 줄이고 성능을 크게 향상시킬 수 있습니다. 캐시는 자주 조회되는 데이터를 메모리에 저장해 DB 부하를 줄여줍니다.

언제 사용하나요?

  • 읽기 요청이 많은 시스템
  • DB 부하를 줄이고 응답 속도를 높이고 싶을 때

Spring Boot 예시

Redis를 사용해 재고를 캐싱하고 동시성을 제어해보겠습니다.

@Service
public class StockService {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    private static final String STOCK_KEY = "stock:product:1";

    public void decreaseStock(int quantity) {
        String stockStr = redisTemplate.opsForValue().get(STOCK_KEY);
        int stock = stockStr != null ? Integer.parseInt(stockStr) : 100;

        if (stock >= quantity) {
            redisTemplate.opsForValue().set(STOCK_KEY, String.valueOf(stock - quantity));
            System.out.println("재고 감소! 남은 재고: " + (stock - quantity));
        } else {
            throw new RuntimeException("재고 부족!");
        }
    }
}
 

여기서는 단순히 Redis에 재고를 저장했지만, 실제로는 Redis의 SETNX(Set if Not Exists) 같은 명령어나 분산 락(Distributed Lock)을 활용해 동시성을 더 철저히 제어할 수 있습니다. 비용이 들지만 확장성과 성능 면에서 큰 이점이 있어요.


정리: 어떤 방법을 선택할까?

  • synchronized: 간단하고 비용 없음, 소규모 시스템에 적합
  • 낙관적 락: 충돌이 적을 때 성능 좋음, 무료
  • 비관적 락: 충돌이 많고 데이터 무결성이 중요할 때, 무료지만 성능 저하 주의
  • 캐시 서버: 돈은 들지만 성능과 확장성 극대화

프로젝트의 요구사항과 예산에 따라 적절한 방법을 선택하면 됩니다. 예를 들어, 간단한 앱이라면 synchronized나 낙관적 락으로 충분할 수 있고, 대규모 트래픽을 감당해야 한다면 Redis 같은 캐시 서버를 고려해볼 만해요.

이 글이 동시성 문제 해결에 도움이 되셨길 바랍니다.

반응형

'개발 부트캠프 > 백엔드' 카테고리의 다른 글

[Kafka] Kafka 이용하여 Spring Boot로 메시지 보내고 받기  (0) 2025.02.27
[MSA] OpenFeign  (0) 2025.02.27
[Trace] Jaeger  (0) 2025.02.21
[Log] Log 중앙화  (0) 2025.02.21
[Trace] 핀포인트(Pinpoint)  (0) 2025.02.21
반응형

핀포인트(Pinpoint)란?

Pinpoint는 애플리케이션 성능 모니터링(APM, Application Performance Monitoring)을 위한 오픈소스 도구입니다. 주로 분산 시스템에서 애플리케이션의 성능 문제를 추적하고 분석하는 데 사용됩니다. Pinpoint는 특히 요청 흐름(request tracing)을 시각화하고, 병목 지점을 식별하며, 실시간 모니터링을 제공하는 데 강점을 가지고 있습니다. 한국의 네이버(Naver)에서 개발했으며, 현재는 오픈소스 프로젝트로 GitHub에서 관리되고 있습니다.
 

핀포인트의 주요 기능

  • 분산 추적(Distributed Tracing):
    • 마이크로서비스 환경에서 하나의 요청이 여러 서비스를 거치는 경로를 추적합니다.
    • 예: 사용자 요청 → 프론트엔드 → 백엔드 API → 데이터베이스 호출 과정을 시각화.

  • 실시간 모니터링:
    • 애플리케이션의 응답 시간, 처리량, 오류율 등을 실시간으로 확인.

  • 성능 분석:
    • 느린 요청이나 오류가 발생한 지점을 정확히 파악.
    • 메서드 레벨까지 세부적으로 성능 데이터 제공.

  • 시각화:
    • 웹 기반 대시보드로 트랜잭션 흐름도, 서버 상태, 통계 등을 표시.

  • 경량 에이전트:
    • 애플리케이션에 추가되는 Pinpoint 에이전트가 성능에 미치는 영향을 최소화.

 

핀포인트의 구성 요소

  1. Pinpoint Agent:
    • 모니터링 대상 애플리케이션(Java 기반)에 설치.
    • 애플리케이션의 요청 데이터(호출 스택, 시간 등)와 같은 성능 관련 지표를 수집해 Collector로 전달.
  2. Pinpoint Collector:
    • 에이전트로부터 수집된 데이터를 받아 HBase에 저장.
    • ZooKeeper로 데이터 조정 및 클러스터 관리.
  3. Pinpoint Web:
    • 저장된 데이터를 웹 UI로 시각화.
    • 사용자 인터페이스를 통해 모니터링 및 분석 제공.
  4. HBase:
    • Pinpoint의 백엔드 데이터베이스.
    • 수집된 추적 데이터와 통계를 저장.
  5. ZooKeeper:
    • Collector와 Web 간 데이터 동기화 및 클러스터 상태 관리.

 

사용 사례 및 장 단점

 

사용 사례

  • 대규모 서비스: 네이버, 쿠팡 등 트래픽이 많은 환경에서 사용.
  • 마이크로서비스: 서비스 간 호출 분석.
  • 장애 분석: 느린 응답이나 오류 원인 파악.

장점

  • 코드 수정 최소화: 에이전트를 붙이는 것만으로 모니터링 가능.
  • 상세 추적: 메서드 단위까지 분석.
  • 오픈소스: 무료로 사용 가능, 커뮤니티 지원.

단점

  • 설정 복잡성: HBase, ZooKeeper 등 의존성 설치 필요.
  • 리소스 사용: 대규모 데이터 처리 시 서버 부하 증가.

 

실습 방법

1) 저장소 및 Web 서버 설정

 

1. 자바 설치

apt update

apt install -y openjdk-8-jdk

 

2. HBase 다운로드 및 압축 해제

wget https://archive.apache.org/dist/hbase/1.2.7/hbase-1.2.7-bin.tar.gz

tar zxvf hbase-1.2.7-bin.tar.gz

 

3. HBase 환경 변수 수정

 

vi ./hbase-1.2.7/conf/hbase-env.sh

------------------------------------------------------

# 28번 라인 수정
export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64

# 44, 47, 48 라인 주석 처리 
# export HBASE_OPTS="-XX:+UseConcMarkSweepGC"
# export HBASE_MASTER_OPTS="$HBASE_MASTER_OPTS -XX:Per    mSize=128m -XX:MaxPermSize=128m -XX:ReservedCodeCacheS    ize=256m"
# export HBASE_REGIONSERVER_OPTS="$HBASE_REGIONSERVER_    OPTS -XX:PermSize=128m -XX:MaxPermSize=128m -XX:Reserv    edCodeCacheSize=256m"

 

4. HBase 실행

./hbase-1.2.7/bin/start-hbase.sh
  • HBase 마스터와 리전 서버를 시작. 이 과정에서 ZooKeeper도 함께 시작됨(기본적으로 HBase 1.2.7은 번들 ZooKeeper 포함).

5. HBase 테이블 생성 스크립트 다운로드 및 테이블 생성

wget https://raw.githubusercontent.com/pinpoint-apm/pinpoint/master/hbase/scripts/hbase-create.hbase

./hbase-1.2.7/bin/hbase shell ./hbase-create.hbase
  • Pinpoint에서 제공하는 HBase 테이블 생성 스크립트(hbase-create.hbase)를 다운로드
  • 이 스크립트는 Pinpoint가 데이터를 저장할 HBase 테이블을 정의 및 생성 (예: ApplicationTraceIndex, AgentStat 등)

6. Pinpoint Collector 다운로드 및 실행

wget https://github.com/pinpoint-apm/pinpoint/releases/download/v2.3.3/pinpoint-collector-boot-2.3.3.jar

nohup java -jar -Dpinpoint.zookeeper.address=localhost pinpoint-collector-boot-2.3.3.jar > collector.log 2> collector-error.log &
  • nohup: 프로세스가 터미널 종료 후에도 백그라운드에서 계속 실행되도록 함.
  • java -jar: JAR 파일을 실행.
  • -Dpinpoint.zookeeper.address=localhost: JVM 속성으로 ZooKeeper 주소를 localhost로 설정 (기본 포트 2181 가정).
  • > collector.log: 표준 출력을 collector.log 파일로 리다이렉트.
  • 2> collector-error.log: 표준 에러를 collector-error.log로 리다이렉트.
  • &: 백그라운드에서 실행.

7. Pinpoint Web 다운로드 및 실행

wget https://github.com/pinpoint-apm/pinpoint/releases/download/v2.3.3/pinpoint-web-boot-2.3.3.jar

nohup java -jar -Dpinpoint.zookeeper.address=localhost pinpoint-web-boot-2.3.3.jar > web.log 2> web-error.log &

 

 

8. 8080 포트로 Pinpoint Web 접속

 

 

2) Agent 설정

 

1. 자바 설치

apt update

apt install -y openjdk-17-jdk

 

2. Pinpoint Agent다운로드 및 압축해제

wget https://repo1.maven.org/maven2/com/navercorp/pinpoint/pinpoint-agent/3.0.1/pinpoint-agent-3.0.1.tar.gz

tar zxvf pinpoint-agent-3.0.1.tar.gz

 

3. Pinpoint Agent 환경 설정에서 collector IP 주소 변경

vi ./pinpoint-agent-3.0.1/profiles/release/pinpoint.config

----------------------------------------------------

#23번 라인 수정
profiler.transport.grpc.collector.ip=[Pinpoint Collector IP Adress]

 

4. Pinpoint Agent와 함께 배포한 백엔드 서버(java) 실행

java -jar \
  -javaagent:/root/pinpoint-agent-3.0.1/pinpoint-bootstrap-3.0.1.jar \
  -Dpinpoint.agentId=my-project \
  -Dpinpoint.applicationName=spring-server \
backend-ver-SNAPSHOT.jar

 

요약

Pinpoint는 분산 시스템의 성능을 모니터링하고 요청 흐름을 추적하는 도구로, HBase와 ZooKeeper를 활용해 데이터를 저장하고 시각화합니다. 애플리케이션 성능 문제를 쉽게 파악하고 싶을 때 유용하며, 특히 Java 기반 시스템에서 강력한 기능을 발휘합니다.

반응형

'개발 부트캠프 > 백엔드' 카테고리의 다른 글

[Trace] Jaeger  (0) 2025.02.21
[Log] Log 중앙화  (0) 2025.02.21
[Kafka] Kafka 실행 실습  (0) 2025.02.17
[Spring] 환경 변수 설정  (0) 2025.02.15
[Spring] @Component와 @Bean의 차이점  (0) 2025.02.15
반응형

IoC와 DI: 개념과 Spring Boot에서 의존성 주입 방법

1. IoC(Inversion of Control)란?

IoC(Inversion of Control, 제어의 역전)는 객체의 생성 및 흐름의 제어 권한을 개발자가 직접 관리하는 것이 아니라 프레임워크(Spring Container)가 관리하도록 하는 개념입니다. 이를 통해 결합도를 낮추고 유연한 애플리케이션을 설계할 수 있습니다.

2. DI(Dependency Injection)란?

DI(Dependency Injection, 의존성 주입)는 IoC를 구현하는 방식 중 하나로, 객체 간의 의존성을 직접 생성하는 것이 아니라 외부에서 주입하는 방식입니다. 이를 통해 코드의 유지보수성과 테스트 용이성이 향상됩니다.

Spring Boot에서는 다양한 방식으로 의존성을 주입할 수 있습니다. 주요 방식은 다음과 같습니다:

  1. 생성자 주입(Constructor Injection)
  2. 세터 주입(Setter Injection)
  3. 필드 주입(Field Injection)
  4. 인터페이스 기반 주입(Interface-based Injection)

3. Spring Boot에서 의존성 주입하는 4가지 방법

3.1 생성자 주입 (Constructor Injection)

가장 권장되는 방식으로, final 키워드를 활용할 수 있어 불변성을 유지할 수 있으며, 단위 테스트가 용이합니다.

import org.springframework.stereotype.Service;

@Service
public class UserService {
    private final UserRepository userRepository;

    // 생성자를 통한 의존성 주입
    public UserService(UserRepository userRepository) {
	    this.userRepository = userRepository;
    }

    public void createUser(String name) {
    	userRepository.save(new User(name));
    }
}
import org.springframework.stereotype.Repository;

@Repository
public class UserRepository {
    public void save(User user) {
	    System.out.println("User saved: " + user.getName());
    }
}

3.2 세터 주입 (Setter Injection)

객체를 생성한 후 setter 메서드를 통해 의존성을 주입하는 방식입니다. 선택적인 의존성을 주입할 때 유용하지만, 객체의 상태가 변경될 가능성이 있어 주의가 필요합니다.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {
    private UserRepository userRepository;

    @Autowired
    public void setUserRepository(UserRepository userRepository) {
	    this.userRepository = userRepository;
    }

    public void createUser(String name) {
    	userRepository.save(new User(name));
    }
}

3.3 필드 주입 (Field Injection)

가장 간단한 방법이지만, 필드에 직접 @Autowired를 사용하면 의존성이 명확하게 보이지 않고, 테스트 시 객체의 변경이 어려울 수 있습니다.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    public void createUser(String name) {
	    userRepository.save(new User(name));
    }
}

3.4 인터페이스 기반 주입 (Interface-based Injection)

DI 원칙을 더욱 철저하게 준수하기 위해 인터페이스를 활용하는 방식입니다. 다양한 구현체를 손쉽게 교체할 수 있어 확장성이 뛰어납니다.

import org.springframework.stereotype.Service;

@Service
public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
	    this.userRepository = userRepository;
    }

    public void createUser(String name) {
    	userRepository.save(new User(name));
    }
}
public interface UserRepository {
	void save(User user);
}
import org.springframework.stereotype.Repository;

@Repository
public class UserRepositoryImpl implements UserRepository {
    @Override
    public void save(User user) {
	    System.out.println("User saved: " + user.getName());
    }
}

4. 의존성 주입 방식 비교

방식 장점 단점
생성자 주입 불변성 유지, 테스트 용이 순환 참조 발생 가능
세터 주입 선택적 의존성 주입 가능 불변성이 깨질 위험
필드 주입 코드가 간결 테스트 및 유지보수 어려움
인터페이스 기반 주입 유연성과 확장성 증가 초기 설계가 복잡할 수 있음

 


5. 결론

Spring Boot에서는 다양한 방식으로 의존성을 주입할 수 있으며, 상황에 맞게 적절한 방식을 선택해야 합니다. 일반적으로 생성자 주입이 가장 권장되며, 필요에 따라 세터 주입이나 인터페이스 기반 주입을 활용할 수 있습니다. 필드 주입은 유지보수성과 테스트 용이성 측면에서 권장되지 않습니다.

IoC와 DI를 올바르게 활용하면 결합도를 낮추고 확장성이 높은 애플리케이션을 만들 수 있습니다.

뭔가 쓰는 중…

반응형
반응형

DBCP(Database Connection Pooling)란 무엇인가?

**DBCP(Database Connection Pooling)**는 데이터베이스 연결을 효율적으로 관리하기 위해 사용되는 기술입니다. 데이터베이스와의 연결은 생성과 종료 시 큰 비용이 소모되는데, DBCP는 연결을 미리 생성해두고 재사용함으로써 성능을 향상시킵니다.


왜 DBCP가 필요한가?

  1. 성능 향상:
    • 매번 새로운 연결을 생성하는 비용을 절감합니다.
  2. 리소스 관리:
    • 일정 개수 이상의 연결 생성을 방지하여 데이터베이스 과부하를 방지합니다.
  3. 스레드 안전성:
    • 다중 스레드 환경에서도 안전하게 연결을 관리합니다.

HikariCP란?

HikariCP는 자바에서 가장 널리 사용되는 고성능 커넥션 풀 라이브러리입니다. 속도와 효율성이 뛰어나고 설정이 간단하다는 장점이 있습니다.

주요 특징:

  1. 가볍고 빠름: 낮은 대기 시간과 높은 처리량을 제공합니다.
  2. 효율적인 자원 관리: 연결 누수를 감지하고 빠르게 복구합니다.
  3. 유연한 설정: 다양한 설정 옵션으로 요구 사항에 맞는 커넥션 풀을 구성할 수 있습니다.

HikariCP를 이용한 자바 구현 예제

1. Maven 의존성 추가

<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>5.0.1</version>
</dependency>

2. HikariCP 설정과 데이터베이스 연결

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class HikariCp {
    private static HikariDataSource dataSource;

    static {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mariadb://localhost:3306/dbname");
        config.setUsername("username");
        config.setPassword("userpwd");
        config.addDataSourceProperty("cachePrepStmts", "true");
        config.addDataSourceProperty("prepStmtCacheSize", "250");
        config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
        config.setMinimumIdle(5);
        config.setMaximumPoolSize(10);

        dataSource = new HikariDataSource(config);
    }

    private HikariCp() {
    }

    public static DataSource getDataSource() {
    	return dataSource;
    }

    public static Connection getConnection() throws SQLException {
    	return dataSource.getConnection();
    }

    public static void main(String[] args) {
        String query = "SELECT * FROM users WHERE id = ?";

        try (Connection connection = HikariCp.getConnection();
            PreparedStatement preparedStatement = connection.prepareStatement(query)) {

            preparedStatement.setInt(1, 1);
            ResultSet resultSet = preparedStatement.executeQuery();

            while (resultSet.next()) {
                System.out.println("User ID: " + resultSet.getInt("id"));
                System.out.println("Username: " + resultSet.getString("username"));
            }

        } catch (SQLException e) {
	        e.printStackTrace();
        }
    }
}

3. 주요 설정 옵션

  • setJdbcUrl: 데이터베이스 URL 설정.
  • setUsername: 데이터베이스 사용자 이름.
  • setPassword: 데이터베이스 비밀번호.
  • setMinimumIdle: 유지할 최소 유휴 연결 수.
  • setMaximumPoolSize: 커넥션 풀에서 허용하는 최대 연결 수.
  • addDataSourceProperty: 데이터베이스 연결 속성 추가.

활용 예시

1. 사용자 인증 시스템

  • 로그인 요청 시 사용자 정보를 데이터베이스에서 빠르게 조회.

2. 대용량 데이터 처리

  • 다중 스레드 환경에서 데이터 분석 작업 수행.

3. 실시간 데이터 처리

  • 대기 시간이 중요한 실시간 애플리케이션에서 안정적인 데이터베이스 연결 제공.

결론

HikariCP를 활용한 DBCP는 자바 애플리케이션에서 데이터베이스 연결을 효율적으로 관리하는 데 필수적인 기술입니다. 간단한 설정으로 강력한 성능을 제공하며, 안정성과 확장성을 동시에 만족시킵니다. HikariCP를 프로젝트에 적용하여 더 나은 성능과 자원 관리를 경험해보세요!

 

반응형

'개발 부트캠프 > 백엔드' 카테고리의 다른 글

[Spring] @Controller와 @RestController의 차이  (0) 2025.02.10
[Spring] IoC & DI  (0) 2025.02.04
[Java] Servlet Filter  (0) 2025.01.16
[Java] JSON  (0) 2025.01.16
[Java] HTTP 요청 처리하기  (0) 2025.01.16
반응형

Servlet Filter: 개념과 활용

Servlet Filter는 웹 애플리케이션에서 HTTP 요청 및 응답을 가로채 특정 작업을 수행할 수 있는 기능을 제공합니다. 주로 요청 데이터 검증, 응답 수정, 로깅, 인증 및 권한 검사 등에 사용됩니다.


Servlet Filter란?

주요 특징:

  1. 요청 및 응답 처리: 클라이언트 요청 전/후, 서블릿 호출 전/후에 작업 수행.
  2. 재사용 가능: 여러 서블릿에 동일한 필터를 적용 가능.
  3. 체이닝: 여러 필터를 순서대로 연결하여 처리 가능.

동작 흐름:

  1. 클라이언트가 요청을 보냄.
  2. 필터가 요청을 가로채고 처리.
  3. 필터 체인이 끝나면 서블릿으로 요청 전달.
  4. 서블릿에서 응답을 생성하여 필터 체인으로 반환.
  5. 필터가 응답을 처리하고 클라이언트로 반환.

자바에서 Servlet Filter 구현

1. 필터 인터페이스 구현

Servlet Filter를 구현하려면 javax.servlet.Filter 인터페이스를 구현해야 합니다.

예시: 로그인 인증 필터

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class AuthenticationFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    // 초기화 작업 (필요 시 구현)
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        // 세션에서 로그인 정보 확인
        if (httpRequest.getSession().getAttribute("user") == null) {
        	// 로그인되지 않은 경우 로그인 페이지로 리다이렉트
	        httpResponse.sendRedirect("/login.jsp");
        } else {
        	// 로그인된 경우 요청을 다음 필터나 서블릿으로 전달
    	    chain.doFilter(request, response);
        }
    }

    @Override
    public void destroy() {
    // 자원 정리 작업 (필요 시 구현)
    }
}

2. web.xml에 필터 등록

필터를 서블릿 컨테이너에 등록하고 URL 패턴을 매핑합니다.

<filter>
	<filter-name>AuthenticationFilter</filter-name>
	<filter-class>com.example.AuthenticationFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>AuthenticationFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

3. 애너테이션 기반 등록 (대체 방식)

Servlet 3.0 이상에서는 @WebFilter 애너테이션을 사용하여 필터를 등록할 수 있습니다.

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebFilter(filterName = "AuthenticationFilter", urlPatterns = {"/protected/*", "/admin/*"})
public class AuthenticationFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        if (httpRequest.getSession().getAttribute("user") == null) {
	        httpResponse.sendRedirect("/login.jsp");
        } else {
    	    chain.doFilter(request, response);
        }
    }
}

이름 및 URL 패턴 지정 이유

필터 이름 지정:

  • 필터 이름(filterName)은 필터를 식별하는 데 사용됩니다. 프로젝트가 커지고 여러 필터를 사용할 때 유지보수성과 가독성을 높이기 위해 필수적입니다.

URL 패턴 지정:

  • urlPatterns는 필터를 적용할 경로를 지정합니다. 특정 요청에만 필터를 적용하여 불필요한 작업을 줄이고 성능을 최적화할 수 있습니다.
    • 예: /protected/*protected 디렉토리 아래의 모든 요청에 필터를 적용합니다.
    • 예: /admin/*는 관리자 전용 URL에만 필터를 적용하여 권한 관리를 강화합니다.

 


로그인 필터의 활용 시나리오

  1. 로그인 검증:
    • 사용자 요청이 들어올 때 세션을 확인하여 로그인 여부를 검증.
    • 로그인되지 않은 사용자를 로그인 페이지로 리다이렉트.
  2. 로깅:
    • 요청 및 응답의 메타데이터를 기록하여 디버깅 및 감사용 로그 생성.
  3. 권한 확인:
    • 사용자의 역할(Role)을 확인하여 권한 없는 요청 차단.

권한 확인 필터 예시:

if (httpRequest.getSession().getAttribute("role") == null ||
!httpRequest.getSession().getAttribute("role").equals("ADMIN")) {
    httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied");
    return;
}

결론

Servlet Filter는 요청과 응답을 가로채서 처리할 수 있는 강력한 도구입니다. 이를 활용하면 인증, 권한 검사, 로깅 등 다양한 작업을 중앙화하여 효율적인 웹 애플리케이션 관리가 가능합니다. 자바에서 Servlet Filter를 구현하여 깔끔하고 확장 가능한 코드를 작성해 보세요!

 

반응형

'개발 부트캠프 > 백엔드' 카테고리의 다른 글

[Spring] IoC & DI  (0) 2025.02.04
[Java] DBCP  (0) 2025.01.16
[Java] JSON  (0) 2025.01.16
[Java] HTTP 요청 처리하기  (0) 2025.01.16
[Java] SQL Injection  (1) 2025.01.10
반응형

JSON: 개념과 활용, 그리고 자바와 자바스크립트에서의 사용법

JSON(JavaScript Object Notation)은 가볍고, 인간과 기계가 모두 읽기 쉬운 데이터 교환 형식입니다. 데이터를 키-값 쌍으로 표현하며, 주로 서버와 클라이언트 간 데이터 교환에 사용됩니다. 이번 글에서는 JSON의 개념, 구조, 자바스크립트와 자바에서 JSON을 다루는 방법을 살펴보겠습니다.


JSON이란 무엇인가?

JSON의 주요 특징:

  1. 간결성: 데이터를 표현하는 간단한 형식.
  2. 가독성: 사람과 기계가 쉽게 읽을 수 있음.
  3. 언어 독립적: 모든 프로그래밍 언어에서 사용 가능.

JSON 예제:

{
    "name": "John Doe",
    "age": 30,
    "isMarried": false,
    "children": ["Anna", "Billy"],
    "address": {
        "street": "123 Main St",
        "city": "New York"
    }
}

자바스크립트에서 JSON 다루기

1. JSON 문자열 생성

const jsonData = {
    name: "John Doe",
    age: 30,
    isMarried: false,
    children: ["Anna", "Billy"],
    address: {
        street: "123 Main St",
        city: "New York"
    }
};

// 객체를 JSON 문자열로 변환
const jsonString = JSON.stringify(jsonData);
console.log(jsonString);

2. JSON 문자열을 객체로 변환

const jsonString = '{"name":"John Doe","age":30}';

// JSON 문자열을 객체로 변환
const jsonObject = JSON.parse(jsonString);
console.log(jsonObject.name); // "John Doe"

3. JSON 데이터 보내고 받기 (AJAX 사용)

// 데이터 전송
fetch("https://example.com/api", {
    method: "POST",
    headers: {
	    "Content-Type": "application/json"
    },
    body: JSON.stringify({ name: "John Doe", age: 30 })
})
.then(response => response.json())
.then(data => console.log(data));

자바에서 JSON 다루기

자바에서는 Jackson Databind 라이브러리를 주로 사용하여 JSON 데이터를 처리합니다.

1. 의존성 추가 (Maven):

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.14.0</version>
</dependency>

2. JSON을 자바 객체로 변환

JSON 문자열:

{
    "name": "John Doe",
    "age": 30,
    "isMarried": false
}

자바 클래스:

public class Person {
    private String name;
    private int age;
    private boolean isMarried;

    // Getters and Setters
    public String getName() {
	    return name;
    }
    public void setName(String name) {
    	this.name = name;
    }
    public int getAge() {
    	return age;
    }
    public void setAge(int age) {
    	this.age = age;
    }
    public boolean isMarried() {
    	return isMarried;
    }
    public void setMarried(boolean married) {
    	isMarried = married;
    }
}

JSON을 객체로 변환:

import com.fasterxml.jackson.databind.ObjectMapper;

public class JsonToObjectExample {
    public static void main(String[] args) {
        String jsonString = "{\"name\":\"John Doe\",\"age\":30,\"isMarried\":false}";

        ObjectMapper objectMapper = new ObjectMapper();

        try {
            Person person = objectMapper.readValue(jsonString, Person.class);
            System.out.println("Name: " + person.getName());
            System.out.println("Age: " + person.getAge());
            System.out.println("Is Married: " + person.isMarried());
        } catch (Exception e) {
	        e.printStackTrace();
        }
    }
}

3. 자바 객체를 JSON으로 변환

import com.fasterxml.jackson.databind.ObjectMapper;

public class ObjectToJsonExample {
    public static void main(String[] args) {
        Person person = new Person();
        person.setName("John Doe");
        person.setAge(30);
        person.setMarried(false);

        ObjectMapper objectMapper = new ObjectMapper();

        try {
            String jsonString = objectMapper.writeValueAsString(person);
            System.out.println("JSON String: " + jsonString);
        } catch (Exception e) {
	        e.printStackTrace();
        }
    }
}

JSON 데이터와 자바 클래스 매핑

복잡한 JSON 구조 예:

{
    "name": "John Doe",
    "age": 30,
    "children": ["Anna", "Billy"],
    "address": {
        "street": "123 Main St",
        "city": "New York"
    }
}

자바 클래스:

import java.util.List;

public class Person {
    private String name;
    private int age;
    private List<String> children;
    private Address address;

    // Getters and Setters
    // ...
}

class Address {
private String street;
private String city;

// Getters and Setters
// ...
}

결론

JSON은 간결하고 효율적인 데이터 교환 형식으로, 자바스크립트와 자바에서 쉽게 사용할 수 있습니다. Jackson 라이브러리를 활용하면 JSON 데이터를 객체로 변환하거나 객체를 JSON으로 변환하는 작업을 간단히 처리할 수 있습니다. 이를 통해 서버-클라이언트 간 데이터 교환을 더욱 효율적으로 관리해 보세요!

 

반응형

'개발 부트캠프 > 백엔드' 카테고리의 다른 글

[Java] DBCP  (0) 2025.01.16
[Java] Servlet Filter  (0) 2025.01.16
[Java] HTTP 요청 처리하기  (0) 2025.01.16
[Java] SQL Injection  (1) 2025.01.10
[Java] 서블릿(Servlet)  (0) 2025.01.08
반응형

자바로 HTTP 요청 처리하기: GET, POST, HEADER 예제

HTTP 통신은 현대 애플리케이션에서 서버와 클라이언트 간 데이터 교환의 핵심입니다. 자바에서는 표준 라이브러리와 외부 라이브러리를 활용하여 HTTP 요청을 쉽게 처리할 수 있습니다. 이번 글에서는 HTTP GET, POST, HEADER를 사용하는 방법과 예제를 소개합니다.


HTTP 요청의 주요 메서드

  1. GET: 서버에서 데이터를 가져오는 데 사용.
  2. POST: 서버에 데이터를 전송하는 데 사용.
  3. HEADER: 요청 헤더를 통해 부가 정보를 서버에 전달.

1. GET 요청

HttpClient를 이용한 GET 요청:

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class HttpGetExample {
    public static void main(String[] args) {
        try {
            String url = "https://jsonplaceholder.typicode.com/posts/1";

            HttpClient client = HttpClient.newHttpClient();
            HttpRequest httpRequest = HttpRequest.newBuilder()
                                        .uri(URI.create(url))
                                        .GET()
                                        .build();

            HttpResponse<String> httpResponse = client.send(httpRequest, HttpResponse.BodyHandlers.ofString());

            System.out.println("Response Code: " + httpResponse.statusCode());
            System.out.println("Response Body: " + httpResponse.body());
        } catch (Exception e) {
	        e.printStackTrace();
        }
    }
}

출력 결과: JSON 데이터를 포함한 서버 응답.


2. POST 요청

HttpClient를 이용한 POST 요청:

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class HttpPostExample {
    public static void main(String[] args) {
        try {
            String url = "https://jsonplaceholder.typicode.com/posts";
            String body = "{"title": "foo", "body": "bar", "userId": 1}";

            HttpClient client = HttpClient.newHttpClient();
            HttpRequest httpRequest = HttpRequest.newBuilder()
                                        .uri(URI.create(url))
                                        .header("Content-Type", "application/json")
                                        .POST(HttpRequest.BodyPublishers.ofString(body))
                                        .build();

            HttpResponse<String> httpResponse = client.send(httpRequest, HttpResponse.BodyHandlers.ofString());

            System.out.println("Response Code: " + httpResponse.statusCode());
            System.out.println("Response Body: " + httpResponse.body());
        } catch (Exception e) {
	        e.printStackTrace();
        }
    }
}

출력 결과: 서버에 전송된 데이터와 응답 메시지.


3. HEADER 추가하기

HttpClient를 이용하여 헤더 추가:

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class HttpHeaderExample {
    public static void main(String[] args) {
        try {
            String url = "https://jsonplaceholder.typicode.com/posts/1";

            HttpClient client = HttpClient.newHttpClient();
            HttpRequest httpRequest = HttpRequest.newBuilder()
                                        .uri(URI.create(url))
                                        .header("Authorization", "Bearer token123")
                                        .GET()
                                        .build();

            HttpResponse<String> httpResponse = client.send(httpRequest, HttpResponse.BodyHandlers.ofString());

            System.out.println("Response Code: " + httpResponse.statusCode());
            System.out.println("Response Body: " + httpResponse.body());
        } catch (Exception e) {
	        e.printStackTrace();
        }
    }
}

출력 결과: 헤더를 포함한 요청에 대한 서버 응답.


결론

자바에서는 기본 API인 HttpClient를 활용하여 간단하고 직관적으로 HTTP 요청을 처리할 수 있습니다. GET, POST, HEADER를 적절히 활용하여 애플리케이션의 요구 사항을 충족해 보세요!

 

반응형

'개발 부트캠프 > 백엔드' 카테고리의 다른 글

[Java] Servlet Filter  (0) 2025.01.16
[Java] JSON  (0) 2025.01.16
[Java] SQL Injection  (1) 2025.01.10
[Java] 서블릿(Servlet)  (0) 2025.01.08
[Java] 계층형 아키텍처 패턴 (Layered Architecture Pattern)  (0) 2025.01.07
반응형

SQL Injection: 개념과 방어 방법

SQL Injection은 사용자가 입력한 데이터를 통해 데이터베이스에 비정상적인 SQL 쿼리를 주입하는 보안 취약점 공격입니다. 이를 통해 공격자는 데이터베이스의 민감한 데이터를 조회, 수정, 삭제하거나 시스템에 대한 제어권을 얻을 수 있습니다.

이 글에서는 SQL Injection의 개념과 바이패스 방법, 그리고 이를 방어하는 다양한 기법을 자바 예제와 함께 설명하겠습니다.


SQL Injection이란?

SQL Injection은 주로 다음과 같은 방식으로 발생합니다:

  • 입력값을 SQL 쿼리 문자열에 그대로 삽입하여 실행.
  • 쿼리의 논리를 변조하여 의도하지 않은 결과를 유도.

예시: 취약한 코드

String query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "';";

공격자가 usernameadmin' --를 입력하면 쿼리는 다음과 같이 변조됩니다:

SELECT * FROM users WHERE username = 'admin' -- ' AND password = '';

--는 SQL에서 주석 처리를 의미하므로 비밀번호 조건이 무시됩니다.


SQL Injection 바이패스 방법

SQL Injection 바이패스는 데이터베이스의 입력 검증과 로직을 우회하여 공격하는 기법입니다.

바이패스 방법 예시:

  1. 논리 우회:
    • 입력값에 논리 연산자(OR 1=1)를 삽입하여 조건을 항상 참으로 만듦.
    • 예: username=admin' OR 1=1 --
  2. 유효성 검사 우회:
    • 특수문자, 인코딩 기법을 활용하여 필터링 로직을 우회.
    • 예: admin' OR '1'='1 (문자열 비교 이용).
  3. 스키마 정보 노출:
    • UNION 쿼리를 사용하여 추가적인 데이터베이스 정보를 조회.
    • 예: admin' UNION SELECT null, username, password FROM users --

SQL Injection 방어 방법

SQL Injection을 방지하기 위해 다음 기법을 사용할 수 있습니다.

1. 입력값 검증

입력값을 철저히 검증하여 예상치 못한 입력을 필터링합니다.

예제:

String username = request.getParameter("username");
String sanitizedUsername = username.replaceAll("[^a-zA-Z0-9]", "");

2. SQL 로직 검증

SQL 쿼리 작성 시 논리적 오류가 발생하지 않도록 구조를 점검합니다.

예제:

if (username.contains("'") || password.contains("'")) {
	throw new IllegalArgumentException("Invalid characters in input");
}

3. Prepared Statement 사용

Prepared Statement는 파라미터화된 쿼리를 사용하여 SQL Injection 공격을 원천적으로 차단합니다.

Statement와의 차이점:

  • Statement: 쿼리와 데이터를 결합하여 실행.
  • Prepared Statement: 쿼리와 데이터를 분리하여 처리.

취약한 Statement 예제:

Statement stmt = connection.createStatement();
String query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "';";
ResultSet rs = stmt.executeQuery(query);

안전한 Prepared Statement 예제:

String query = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement pstmt = connection.prepareStatement(query);
pstmt.setString(1, username);
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();

4. ORM (Object-Relational Mapping) 사용

JPA, Hibernate 같은 ORM 프레임워크를 사용하여 SQL 쿼리를 직접 작성하지 않고 객체 지향적으로 데이터베이스와 상호작용.

예제:

@Entity
public class User {
    @Id
    private Long id;
    private String username;
    private String password;
}

public User findByUsernameAndPassword(String username, String password) {
    return entityManager.createQuery(
    "SELECT u FROM User u WHERE u.username = :username AND u.password = :password", User.class)
    .setParameter("username", username)
    .setParameter("password", password)
    .getSingleResult();
}

마무리: SQL Injection 방어의 중요성

SQL Injection은 잘못된 입력값 처리를 통해 발생하는 심각한 보안 취약점입니다. 이를 방지하려면 입력값 검증, 로직 점검, 그리고 Prepared Statement와 같은 안전한 프로그래밍 기법을 활용해야 합니다. 보안을 염두에 두고 코드를 작성하는 습관을 들여 안전한 애플리케이션을 개발하세요!

 

반응형

'개발 부트캠프 > 백엔드' 카테고리의 다른 글

[Java] JSON  (0) 2025.01.16
[Java] HTTP 요청 처리하기  (0) 2025.01.16
[Java] 서블릿(Servlet)  (0) 2025.01.08
[Java] 계층형 아키텍처 패턴 (Layered Architecture Pattern)  (0) 2025.01.07
[Java] MVC 패턴  (1) 2025.01.07
반응형

서블릿(Servlet)

**서블릿(Servlet)**은 자바 기반 웹 애플리케이션에서 클라이언트의 요청을 처리하고 동적 콘텐츠를 생성하는 서버 측 컴포넌트입니다. HTTP 프로토콜을 기반으로 하며, Java Servlet API를 통해 동작합니다. 주로 HTML, JSON, XML 등의 응답을 생성하여 클라이언트에 반환합니다.

이 글에서는 서블릿의 개념과 함께 클라이언트에서 데이터를 보내고 받는 방법, 그리고 서블릿에서 JSP를 통해 데이터를 반환하는 방법을 예제와 함께 설명합니다.


서블릿의 주요 기능

  1. 클라이언트 요청 처리:
    • GET, POST, PUT 등의 HTTP 요청을 처리합니다.
  2. 데이터 처리 및 로직 수행:
    • 서버에서 필요한 비즈니스 로직을 수행하고 데이터베이스와 상호작용합니다.
  3. 응답 생성:
    • 클라이언트가 이해할 수 있는 형식(HTML, JSON 등)으로 데이터를 반환합니다.

클라이언트에서 서블릿에 데이터 보내는 방법

클라이언트는 GET 또는 POST 요청을 통해 데이터를 서블릿에 보낼 수 있습니다.

1. GET 요청을 통한 데이터 전송

GET 요청은 URL에 데이터를 쿼리 문자열(Query String)로 포함하여 전송합니다.

- URL뒤에 ?와 &이용하여 데이터를 포함

 

클라이언트 요청 예시:

<a href="http://localhost:8080/MyServlet?name=John&age=30">Send Data</a>

서블릿에서 GET 요청 처리:

request.getParameter()이용하여 클라이언트가 보낸 데이터를 받습니다.

import jakarta.servlet.*;
import jakarta.servlet.http.*;
import java.io.IOException;

public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
	    // 데이터 수신
        String name = request.getParameter("name");
        String age = request.getParameter("age");

        // 응답 작성
        response.setContentType("text/html");
        response.getWriter().println("<h1>Name: " + name + ", Age: " + age + "</h1>");
    }
}

2. POST 요청을 통한 데이터 전송

POST 요청은 데이터를 요청 본문(Body)에 포함하여 전송합니다.

- form 태그 이용하여 action에 URL, method에 POST 입력

- input 태그 이용하여 입력 받은 데이터를 요청 본문(Body)에 포함 

 

클라이언트 요청 예시:

<form action="http://localhost:8080/MyServlet" method="POST">
    <input type="text" name="name" placeholder="Enter your name">
    <input type="number" name="age" placeholder="Enter your age">
    <button type="submit">Submit</button>
</form>

서블릿에서 POST 요청 처리:

request.getParameter()이용하여 클라이언트가 보낸 데이터를 받습니다.

import jakarta.servlet.*;
import jakarta.servlet.http.*;
import java.io.IOException;

public class MyServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 데이터 수신
        String name = request.getParameter("name");
        String age = request.getParameter("age");

        // 응답 작성
        response.setContentType("text/html");
        response.getWriter().println("<h1>Name: " + name + ", Age: " + age + "</h1>");
    }
}

서블릿에서 클라이언트에 데이터 보내는 방법 (JSP 사용)

서블릿은 JSP와 연동하여 동적 웹 페이지를 생성할 수 있습니다.

- request.setAttribute("dataName", Data)를 이용하여 JSP에 데이터 전송

 

1. 서블릿에서 JSP로 데이터 전달:

import jakarta.servlet.*;
import jakarta.servlet.http.*;
import java.io.IOException;

public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 데이터 설정
        request.setAttribute("message", "Hello from Servlet!");

        // JSP로 포워딩
        RequestDispatcher dispatcher = request.getRequestDispatcher("/display.jsp");
        dispatcher.forward(request, response);
    }
}

2. JSP에서 데이터 출력:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<title>Servlet to JSP</title>
</head>
<body>
<h1>Message from Servlet: ${message}</h1>
<h1>Message from Servlet: <%= message></h1>
</body>
</html>

 


서블릿과 JSP의 활용 예시

  1. 로그인 시스템:
    • 클라이언트가 로그인 폼에서 입력한 데이터를 서블릿으로 보내 검증 후 JSP로 결과를 반환.
  2. 데이터 조회 및 출력:
    • 데이터베이스에서 조회한 결과를 서블릿에서 처리하고 JSP에서 테이블 형식으로 출력.
  3. RESTful API와 연동:
    • 클라이언트에서 서블릿으로 JSON 데이터를 보내고 처리한 결과를 JSP 또는 JSON 형식으로 응답.

마치며

서블릿은 자바 기반 웹 애플리케이션에서 중요한 역할을 합니다. GET과 POST 요청을 통해 클라이언트와 데이터를 주고받고, JSP를 활용해 동적 콘텐츠를 생성하는 방법을 이해하면 더욱 효율적인 웹 개발이 가능합니다. 이를 실습하며 능력을 키워보세요!

 

반응형

'개발 부트캠프 > 백엔드' 카테고리의 다른 글

[Java] HTTP 요청 처리하기  (0) 2025.01.16
[Java] SQL Injection  (1) 2025.01.10
[Java] 계층형 아키텍처 패턴 (Layered Architecture Pattern)  (0) 2025.01.07
[Java] MVC 패턴  (1) 2025.01.07
[Java] SOLID 원칙  (0) 2025.01.07
반응형

계층형 아키텍처 패턴

소프트웨어 설계에서 Layered Architecture (계층형 아키텍처) 패턴은 소프트웨어를 서로 독립적인 계층으로 나누어 설계하는 구조입니다. 이 글에서는 Layered 아키텍처의 개념, 필요성, 그리고 자바에서의 구현 방법을 예제를 통해 설명하겠습니다.


Layered 아키텍처 패턴이란?

Layered 아키텍처 패턴은 애플리케이션을 역할에 따라 계층으로 나누고, 각 계층이 특정한 책임만을 가지도록 설계하는 패턴입니다. 일반적으로 다음 네 가지 주요 계층으로 구성됩니다:

  1. Presentation Layer (표현 계층):
    • 사용자 인터페이스와 상호작용을 담당합니다.
    • 사용자 입력을 처리하고 응답을 표시합니다.
    • ex) UserController.java
  2. Application Layer (애플리케이션 계층):
    • 비즈니스 로직을 실행하고 데이터를 조합하여 처리합니다.
    • 비즈니스 규칙과 워크플로우를 정의합니다.
    • ex) UserService.java
  3. Domain Layer (도메인 계층):
    • 핵심 도메인 로직과 비즈니스 엔터티를 관리합니다.
    • 데이터의 상태와 관련된 규칙을 처리합니다.
    • ex) User.java
  4. Data Access Layer (데이터 접근 계층):
    • 데이터베이스와 상호작용하여 데이터를 읽고 씁니다.
    • 저장소 또는 외부 시스템과의 통신을 담당합니다.
    • ex) UserDao.java

Layered 아키텍처의 필요성

  1. 모듈화:
    • 각 계층이 독립적으로 개발 및 유지보수될 수 있습니다.
  2. 가독성:
    • 계층 간 역할이 명확히 구분되어 코드 이해가 쉽습니다.
  3. 재사용성:
    • 특정 계층을 다른 프로젝트에서 재사용할 수 있습니다.
  4. 테스트 용이성:
    • 계층별로 개별적인 테스트가 가능하여 품질이 향상됩니다.
  5. 확장성:
    • 새로운 기능 추가 시 기존 계층에 최소한의 영향을 줍니다.

자바에서 Layered 아키텍처 구현 예제

사용자 정보를 관리하는 간단한 애플리케이션을 Layered 아키텍처로 구현해 보겠습니다.

1. Presentation Layer (표현 계층)

public class UserController {
	private final UserService userService;

    public UserController(UserService userService) {
	    this.userService = userService;
    }

    public void createUser(String name, String email) {
    	userService.addUser(name, email);
    	System.out.println("User created successfully.");
    }

    public void displayUsers() {
    	userService.getAllUsers().forEach(user ->
    	System.out.println("Name: " + user.getName() + ", Email: " + user.getEmail()));
    }
}

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

import java.util.List;

public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
	    this.userRepository = userRepository;
    }

    public void addUser(String name, String email) {
    	User user = new User(name, email);
    	userRepository.save(user);
    }

    public List<User> getAllUsers() {
    	return userRepository.findAll();
    }
}

3. Domain Layer (도메인 계층)

public class User {
    private String name;
    private String email;

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() {
	    return name;
    }

    public void setName(String name) {
    	this.name = name;
    }

    public String getEmail() {
    	return email;
    }

    public void setEmail(String email) {
    	this.email = email;
    }
}

4. Data Access Layer (데이터 접근 계층)

import java.util.ArrayList;
import java.util.List;

public class UserRepository {
    private final List<User> users = new ArrayList<>();

    public void save(User user) {
	    users.add(user);
    }

    public List<User> findAll() {
    	return new ArrayList<>(users);
    }
}

5. Main 클래스

public class LayeredArchitectureDemo {
    public static void main(String[] args) {
        UserRepository userRepository = new UserRepository();
        UserService userService = new UserService(userRepository);
        UserController userController = new UserController(userService);

        // 사용자 생성
        userController.createUser("Alice", "alice@example.com");
        userController.createUser("Bob", "bob@example.com");

        // 사용자 목록 표시
        userController.displayUsers();
    }
}
출력 결과:
 
User created successfully.
User created successfully.
Name: Alice, Email: alice@example.com
Name: Bob, Email: bob@example.com

Layered 아키텍처의 활용 예시

  1. 기업 애플리케이션:
    • Spring Framework를 활용한 계층형 구조의 웹 애플리케이션.
  2. 마이크로서비스:
    • 서비스 간 역할을 명확히 나누어 각 서비스의 계층을 독립적으로 관리.
  3. 모바일 백엔드:
    • 데이터베이스와의 통신, 비즈니스 로직 처리, API 응답 구성을 분리하여 효율적인 유지보수.

마치며

Layered 아키텍처 패턴은 애플리케이션을 명확하게 구조화하여 유지보수성과 확장성을 크게 향상시킵니다. 자바에서 이를 구현하여 더 나은 소프트웨어를 개발해 보세요!

 

반응형

'개발 부트캠프 > 백엔드' 카테고리의 다른 글

[Java] SQL Injection  (1) 2025.01.10
[Java] 서블릿(Servlet)  (0) 2025.01.08
[Java] MVC 패턴  (1) 2025.01.07
[Java] SOLID 원칙  (0) 2025.01.07
[Java] JDBC (Java Database Connectivity)  (0) 2025.01.07
반응형

MVC 패턴

소프트웨어 설계에서 MVC(Model-View-Controller) 패턴은 애플리케이션을 세 가지 역할로 나누어 효율적이고 유지보수하기 쉬운 구조를 제공합니다. 이 글에서는 MVC 패턴의 개념, 필요성, 그리고 자바에서의 구현 방법과 예제를 살펴보겠습니다.


MVC 패턴이란 무엇인가?

**MVC(Model-View-Controller)**는 애플리케이션을 세 가지 주요 컴포넌트로 분리하는 디자인 패턴입니다:

  1. Model (모델):
    • 데이터와 비즈니스 로직을 관리합니다.
    • 애플리케이션의 상태를 표현하며, 데이터베이스와의 상호작용을 담당합니다.
  2. View (뷰):
    • 사용자 인터페이스를 담당합니다.
    • 모델 데이터를 화면에 표시하고, 사용자의 입력을 컨트롤러에 전달합니다.
  3. Controller (컨트롤러):
    • 사용자 입력을 처리하고, 이를 모델과 뷰로 전달합니다.
    • 모델과 뷰 간의 흐름을 조정하는 중간 역할을 합니다.

MVC 패턴이 왜 필요한가?

  1. 유지보수 용이성:
    • 모델, 뷰, 컨트롤러가 각각 독립적으로 변경 가능하여 유지보수가 쉽습니다.
  2. 모듈화:
    • 역할별로 코드가 분리되어 가독성이 향상됩니다.
  3. 확장성:
    • 새로운 기능 추가 시 기존 코드에 미치는 영향을 최소화합니다.
  4. 테스트 용이성:
    • 모델, 뷰, 컨트롤러를 개별적으로 테스트할 수 있어 품질이 향상됩니다.

자바에서 MVC 패턴 구현 방법

간단한 애플리케이션을 통해 MVC 패턴을 구현해 보겠습니다. 아래 예제는 학생 정보를 표시하는 애플리케이션입니다.

1. Model (모델)

public class Student {
    private String name;
    private int rollNo;

    public Student(String name, int rollNo) {
        this.name = name;
        this.rollNo = rollNo;
    }

    public String getName() {
	    return name;
    }

    public void setName(String name) {
    	this.name = name;
    }

    public int getRollNo() {
    	return rollNo;
    }

    public void setRollNo(int rollNo) {
    	this.rollNo = rollNo;
    }
}

2. View (뷰)

public class StudentView {
    public void printStudentDetails(String studentName, int studentRollNo) {
        System.out.println("Student:");
        System.out.println("Name: " + studentName);
        System.out.println("Roll No: " + studentRollNo);
    }
}

3. Controller (컨트롤러)

public class StudentController {
    private Student model;
    private StudentView view;

    public StudentController(Student model, StudentView view) {
        this.model = model;
        this.view = view;
    }

    public void setStudentName(String name) {
	    model.setName(name);
    }

    public String getStudentName() {
    	return model.getName();
    }

    public void setStudentRollNo(int rollNo) {
    	model.setRollNo(rollNo);
    }

    public int getStudentRollNo() {
    	return model.getRollNo();
    }

    public void updateView() {
    	view.printStudentDetails(model.getName(), model.getRollNo());
    }
}

4. Main 클래스

public class MVCPatternDemo {
    public static void main(String[] args) {
        // 모델 생성
        Student model = new Student("John", 10);

        // 뷰 생성
        StudentView view = new StudentView();

        // 컨트롤러 생성
        StudentController controller = new StudentController(model, view);

        // 초기 데이터 출력
        controller.updateView();

        // 데이터 업데이트
        controller.setStudentName("Doe");
        controller.setStudentRollNo(20);

        // 업데이트된 데이터 출력
        controller.updateView();
    }
}
출력 결과:
 
Student:
Name: John
Roll No: 10
 
Student:
Name: Doe
Roll No: 20

MVC 패턴의 다양한 활용 예시

  1. 웹 애플리케이션 개발:
    • Spring Framework에서 MVC 구조를 사용하여 컨트롤러, 서비스, 뷰를 구현.
  2. 데스크톱 애플리케이션:
    • JavaFX나 Swing 기반의 UI 애플리케이션에서 MVC 적용.
  3. 모바일 애플리케이션:
    • Android 개발에서 Activity를 컨트롤러, XML 레이아웃을 뷰, 비즈니스 로직을 모델로 구현.

마치며

MVC 패턴은 애플리케이션을 구조화하고 유지보수를 쉽게 만들어주는 강력한 도구입니다. 자바에서 이를 효과적으로 구현하여 더 나은 소프트웨어 설계를 경험해 보세요!

 

반응형
반응형

객체 지향 설계 원칙: SOLID

객체 지향 설계에서 SOLID 원칙은 유지보수성과 확장성이 높은 소프트웨어를 개발하기 위해 지켜야 할 다섯 가지 기본 원칙을 의미합니다. 이 글에서는 SOLID의 각 원칙을 자세히 설명하고, 이러한 원칙이 왜 중요한지 살펴보겠습니다.


SOLID란 무엇인가?

SOLID는 객체 지향 설계의 다섯 가지 원칙을 정의한 약어로, 다음과 같은 원칙들로 구성됩니다:

  1. S: 단일 책임 원칙 (Single Responsibility Principle)
  2. O: 개방-폐쇄 원칙 (Open/Closed Principle)
  3. L: 리스코프 치환 원칙 (Liskov Substitution Principle)
  4. I: 인터페이스 분리 원칙 (Interface Segregation Principle)
  5. D: 의존성 역전 원칙 (Dependency Inversion Principle)

이 원칙들은 소프트웨어를 더 유연하고 유지보수 가능하게 만들어줍니다.


1. 단일 책임 원칙 (SRP)

클래스는 단 하나의 책임만 가져야 한다.

설명:

클래스는 하나의 역할만 가져야 하며, 변경의 이유가 하나여야 합니다. 여러 책임이 한 클래스에 섞이면 수정이 어려워지고, 코드 재사용성이 떨어집니다.

예제:
class ReportGenerator {
    public void generateReport() {
    // 보고서 생성 로직
    }

    public void printReport() {
    // 보고서 출력 로직
    }
}

위 클래스는 보고서 생성과 출력 두 가지 책임을 가지고 있습니다. 이를 분리하면:

class ReportGenerator {
    public void generateReport() {
    // 보고서 생성 로직
    }
}

class ReportPrinter {
    public void printReport() {
    // 보고서 출력 로직
    }
}
장점:
  • 클래스가 더 간결해짐.
  • 특정 책임의 변경이 다른 부분에 영향을 미치지 않음.

2. 개방-폐쇄 원칙 (OCP)

클래스는 확장에는 열려 있어야 하지만, 수정에는 닫혀 있어야 한다.

설명:

새로운 기능이 추가될 때 기존 코드를 수정하지 않고 확장할 수 있어야 합니다. 이를 통해 기존 코드의 안정성을 보장할 수 있습니다.

예제:

기존 코드:

class Notification {
    public void send(String type) {
        if (type.equals("Email")) {
	        System.out.println("Send Email");
        } else if (type.equals("SMS")) {
    	    System.out.println("Send SMS");
        }
    }
}

OCP를 적용:

interface Notification {
	void send();
}

class EmailNotification implements Notification {
    @Override
    public void send() {
	    System.out.println("Send Email");
    }
}

class SMSNotification implements Notification {
    @Override
    public void send() {
	    System.out.println("Send SMS");
    }
}
장점:
  • 새로운 알림 타입 추가 시 기존 코드를 수정할 필요 없음.

3. 리스코프 치환 원칙 (LSP)

하위 클래스는 상위 클래스에서 기대하는 동작을 모두 수행할 수 있어야 한다.

설명:

상위 클래스의 객체를 하위 클래스 객체로 대체해도 프로그램의 동작에 문제가 없어야 합니다.

예제:

위반 사례:

class Rectangle {
    private int width;
    private int height;

    public void setWidth(int width) {
	    this.width = width;
    }

    public void setHeight(int height) {
    	this.height = height;
    }

    public int getArea() {
    	return width * height;
    }
}

class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
	    super.setWidth(width);
    	super.setHeight(width);
    }

    @Override
    public void setHeight(int height) {
    	super.setWidth(height);
    	super.setHeight(height);
	}
}

위 코드에서는 사각형(Rectangle) 객체를 기대하는 코드가 정사각형(Square) 객체에서 의도치 않은 동작을 경험할 수 있습니다.

해결:

직사각형과 정사각형을 별도 클래스로 설계.


4. 인터페이스 분리 원칙 (ISP)

클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.

설명:

하나의 거대한 인터페이스 대신, 더 작고 구체적인 인터페이스로 분리해야 합니다.

예제:

위반 사례:

interface Worker {
    void work();
    void eat();
}

class Robot implements Worker {
    @Override
    public void work() {
	    System.out.println("Robot working");
    }

    @Override
    public void eat() {
    	// 로봇은 먹지 않음
    }
}

ISP 적용:

interface Workable {
	void work();
}

interface Eatable {
	void eat();
}

class Robot implements Workable {
    @Override
    public void work() {
	    System.out.println("Robot working");
    }
}
장점:
  • 불필요한 메서드 의존 제거.

5. 의존성 역전 원칙 (DIP)

고수준 모듈은 저수준 모듈에 의존해서는 안 된다. 둘 다 추상화에 의존해야 한다.

설명:

구체적인 구현보다 인터페이스와 같은 추상화에 의존하도록 설계해야 합니다.

예제:

위반 사례:

class Keyboard {}
class Monitor {}

class Computer {
    private Keyboard keyboard;
    private Monitor monitor;

    public Computer() {
        this.keyboard = new Keyboard();
        this.monitor = new Monitor();
    }
}

DIP 적용:

interface InputDevice {}
class Keyboard implements InputDevice {}

interface OutputDevice {}
class Monitor implements OutputDevice {}

class Computer {
    private InputDevice inputDevice;
    private OutputDevice outputDevice;

    public Computer(InputDevice inputDevice, OutputDevice outputDevice) {
    	this.inputDevice = inputDevice;
    	this.outputDevice = outputDevice;
    }
}
장점:
  • 의존성 주입을 통해 유연성 증가.

SOLID 원칙이 필요한 이유

  1. 유지보수성: 코드를 더 쉽게 수정하고 확장할 수 있습니다.
  2. 재사용성: 모듈화된 설계를 통해 코드 재사용이 가능.
  3. 확장성: 요구사항 변경에 유연하게 대응.
  4. 가독성: 명확하고 직관적인 코드 작성.

SOLID 원칙을 준수하면, 변화에 강하고 확장 가능한 소프트웨어를 설계할 수 있습니다. 이를 실천함으로써 소프트웨어 개발의 품질을 한 단계 높여보세요!

 

 

반응형

+ Recent posts