목차
- Command Pattern
- RDBMS
- Transaction(lsolation level)
오늘 디자인 패턴과 JavaScript에서 상속, 추상클래스, 메서드 오버라이딩, 제네릭 부분들을 이해하며 command Pattern이 가지는 이점에 대해서 알게 되었습니다.
커맨트 패턴은 기본적으로 크게 3가지로 분류됨을 알고 있었습니다.
- 생성 패턴
- 구조 패턴
- 행위 패턴
생성 패턴 (Creational Patterns)
생성 패턴은 객체 생성과 관련된 문제를 해결하는 패턴입니다. 객체를 생성하는 방법을 추상화하여, 객체를 직접 생성하는 코드에서 발생할 수 있는 복잡성과 의존성을 줄여줍니다. 이러한 패턴은 객체 생성 방식에 유연성을 부여하며, 객체의 생성 과정을 캡슐화합니다. 주요 패턴으로는 싱글턴 패턴, 팩토리 메서드 패턴, 추상 팩토리 패턴, 빌더 패턴, 프로토타입 패턴 등이 있습니다.
구조 패턴 (Structural Patterns)
구조 패턴은 객체나 클래스 간의 관계를 정의하여 시스템의 구조를 효율적으로 만들 수 있도록 도와줍니다. 이는 복잡한 시스템을 더 간단하고, 재사용 가능하며, 유연하게 설계할 수 있게 합니다. 클래스와 객체들의 구조적인 문제를 해결하는 데 사용되며, 객체들의 관계를 개선하거나 확장할 수 있습니다. 대표적인 패턴으로는 어댑터 패턴, 브리지 패턴, 컴포지트 패턴, 데코레이터 패턴, 퍼사드 패턴 등이 있습니다.
행위 패턴 (Behavioral Patterns) -> 내가 할 패턴!
행위 패턴은 객체들 간의 상호작용과 책임을 어떻게 분배할지에 대한 패턴입니다. 이는 객체들이 서로 통신하고 협력하는 방식에 대한 규칙을 정의하여, 시스템 내에서 일어나는 동작을 더 효율적으로 처리합니다. 객체들의 역할과 책임을 분리하고, 복잡한 로직을 단순화하며, 동적인 변화를 관리하는 데 유용합니다. 책임 연쇄 패턴, 커맨드 패턴, 옵저버 패턴, 상태 패턴, 템플릿 메서드 패턴 등이 주요 패턴으로 포함됩니다.
자주 사용했던 디자인 패턴들은, 사용자 권한 체크를 빠르게 효율적으로 하기 위한 싱글톤 패턴, MVC 패턴을 사용하기 위한 옵저버 패턴등을 사용해봤기 때문에 이번에 배운 커맨드 패턴은 조금 신기했습니다.
아래의 코드는 커맨드 패턴의 진화형입니다.
// Command 는 작업(동작) 단위로 이루어지기 때문에 Input 과 Output 은 제네릭으로 열어둡니다.
abstract class BaseCommand<TRequest, TResult> {
execute(dto: TRequest): TResult {
// 공통 기능 처리 (데코레이터 처리, 로깅 등)
this.onConfigure(dto);
return this.onExecute(dto);
}
// 비즈니스 로직 구현에서 필요시 초기화 작업 구현
protected onConfigure(dto: TRequest) {}
// 비즈니스 로직을 위해 반드시 구현하도록 강제
protected abstract onExecute(dto: TRequest): TResult;
}
이 코드는 커맨드 패턴을 진화시킨 형태로, 비즈니스 로직을 BaseCommand 클래스에서 공통 처리하고, 구체적인 로직은 onExecute 메서드에서 서브클래스가 구현하도록 합니다. execute 메서드는 공통 로직을 처리한 후 onExecute를 호출하여 실제 비즈니스 로직을 실행하는 구조입니다. 이를 통해 비즈니스 로직과 공통 로직을 분리하여 유지보수성과 재사용성을 높이며, 디버깅 시 onExecute만 확인하면 되어 흐름 추적이 용이해집니다.
또한, onConfigure 메서드는 초기화 작업을 처리하는 부분으로, 필요에 따라 서브클래스에서 구현할 수 있습니다. 이 패턴은 공통 기능을 재사용하고 새로운 커맨드를 추가할 때 기존 로직을 변경하지 않고도 비즈니스 로직만 추가하면 되는 장점이 있습니다.
Command Pattern을 실제 코드를 작성하면서 구성을 한번 파악해보겠습니다.
현재 TvRemoteController 클래스는 TV의 상태와 동작을 밀접하게 결합하여 구현하고 있어, 유지보수성과 확장성이 떨어집니다. 각 동작(전원 토글, 볼륨 조정, 채널 변경)은 TvRemoteController 클래스 내에 하드코딩되어 있어, 새로운 기능을 추가하거나 기존 기능을 변경할 때마다 해당 클래스 수정이 필요합니다.
또한, 비즈니스 로직과 제어 로직이 혼합되어 있어 코드가 복잡해지고, 상태 변경에 대한 공통 처리(로깅, 트랜잭션 등)를 일일이 각 메서드에 추가해야 하므로 관리가 어려워집니다. 이를 해결하려면 커맨드 패턴을 적용하여 동작을 독립적인 객체로 분리하고, 공통 처리를 중앙에서 관리하는 방식이 필요합니다.
class TeleVision {
powerOn: boolean;
volume: number;
channel: number;
}
class TvRemoteController {
tv: TeleVision;
public constructor(tv: TeleVision) {
this.tv = tv;
}
public PowerSwitch() {
console.log("전자기파 발산");
this.tv.powerOn = !this.tv.powerOn;
}
public VolumeChange(is_up: boolean) {
console.log("전자기파 발산");
if (!this.tv.powerOn) {
return;
}
this.tv.volume += isUp ? 1 : -1;
}
public ChangeChannel(channel_number: number) {
console.log("전자기파 발산");
if (!this.tv.powerOn) {
return;
}
this.tv.channel = channel_number;
}
}
BaseCommand 실행 흐름
Execute: 커맨드 실행의 진입점. 공통 기능
OnConfigure: 실행 전, 필요한 초기 작업 수행. 비즈니스 로직
OnExecute: 비즈니스 로직의 실제 구현부
// BaseCommand.ts
abstract class BaseCommand<TRequest, TResult, TContext> {
protected context: TContext;
constructor(context: TContext) {
this.context = context;
}
execute(dto: TRequest): TResult {
this.onConfigure(dto);
if (this.canExecute(dto)) {
return this.onExecute(dto);
}
// canExecute가 false일 때 명시적으로 false를 반환
return false as unknown as TResult;
//return undefined as unknown as TResult;
}
protected onConfigure(dto: TRequest): void {}
protected abstract canExecute(dto: TRequest): boolean;
protected abstract onExecute(dto: TRequest): TResult;
}
export default BaseCommand;
해당 코드에서 개선된 방식은 커맨드 패턴을 활용하여 각 기능(전원 토글, 볼륨 조정, 채널 변경 등)을 독립적인 액션으로 분리하고, 상속과 다형성을 통해 공통된 동작을 관리하는 구조를 사용한 것입니다. 이 구조가 잘 적용되었으며, 코드가 확장 가능하고 유지보수가 쉬워졌습니다. 각각의 클래스와 흐름을 이해해보겠습니다.
Command Pattern을 이용한 TeleVision 구현
- 공통 인터페이스 (
IRemoteControlRequest
):- 모든 커맨드에서 사용할 수 있는 공통 요청 인터페이스를 정의했습니다. 이를 통해 전원, 볼륨 조정, 채널 변경 등에 필요한 정보를 통합적으로 전달할 수 있습니다.
RemoteControlAction
클래스 (기본 액션 클래스):BaseCommand
를 상속받아RemoteControlAction
클래스가 기본적인 액션의 틀을 제공합니다. 이 클래스는 텔레비전 객체의 상태를 조작하는 기본적인onExecute
메서드를 정의하고, 이를 각 액션 클래스에서 상속받아 구체적인 로직을 구현할 수 있게 했습니다.
ToggleAction
클래스 (전원 토글):ToggleAction
은 TV의 전원을 켜고 끄는 역할을 합니다.onExecute
메서드에서 전원을 토글하며, 상태를 변경하고 그 결과를 로그로 출력합니다.
VolumeAction
클래스 (볼륨 조정):VolumeAction
은 TV가 켜져 있을 때만 볼륨을 조정하도록 합니다.onExecute
메서드에서volume_up
이 참이면 볼륨을 증가시키고, 그렇지 않으면 감소시킵니다.
ChannelAction
클래스 (채널 변경):ChannelAction
은 TV가 켜져 있을 때만 채널을 변경합니다.channel_number
가 제공된 경우에만 채널을 변경하며, 기본적으로 부모 클래스의onExecute
를 호출한 후 실제 채널 변경 작업을 합니다.
IRemoteControlRequest
인터페이스
interface IRemoteControlRequest {
channel_number?: number;
volume_up?: boolean;
}
- 이 인터페이스는 각 액션에서 필요로 하는 매개변수들을 정의합니다. 예를 들어, 볼륨 증가/감소를 위한
volume_up
플래그와 채널 변경을 위한channel_number
를 담고 있습니다.
RemoteControlAction
클래스
class RemoteControlAction extends BaseCommand<IRemoteControlRequest, boolean, TeleVision> {
protected canExecute(dto: IRemoteControlRequest): boolean {
return true;
}
protected onExecute(dto: IRemoteControlRequest): boolean {
console.log("Action executed on TV");
return true;
}
}
BaseCommand
를 상속받고 있으며,IRemoteControlRequest
를 인수로 받는execute
메서드를 사용합니다.canExecute
메서드는 이 액션을 실행할 수 있는지 여부를 판단하며, 기본적으로 항상true
를 반환합니다.onExecute
메서드는 실제 TV 동작을 수행하는 부분이며,RemoteControlAction
클래스에서는 기본적인 로깅만 수행합니다.
ToggleAction
클래스 (전원 토글)
class ToggleAction extends RemoteControlAction {
protected onExecute(dto: IRemoteControlRequest): boolean {
super.onExecute(dto);
this.context.powerOn = !this.context.powerOn;
console.log(`TV power is now ${this.context.powerOn ? 'ON' : 'OFF'}`);
return this.context.powerOn;
}
}
ToggleAction
클래스는 TV의 전원을 토글하는 액션을 구현합니다.super.onExecute(dto)
를 호출하여 기본 로깅 작업을 처리한 뒤,this.context.powerOn
을 반전시켜 전원 상태를 변경합니다.
VolumeAction
클래스 (볼륨 조정)
class VolumeAction extends RemoteControlAction {
protected canExecute(dto: IRemoteControlRequest): boolean {
return this.context.powerOn;
}
protected onExecute(dto: IRemoteControlRequest): boolean {
if (!this.context.powerOn) {
return false;
}
const isUp = dto.volume_up || false;
this.context.volume += isUp ? 1 : -1;
console.log(`Current volume: ${this.context.volume}`);
return true;
}
}
VolumeAction
클래스는 볼륨을 조정하는 액션을 처리합니다.canExecute
메서드에서 TV가 켜져 있는지 확인하고, 켜져 있을 때만 볼륨을 변경합니다.onExecute
메서드는volume_up
플래그에 따라 볼륨을 증가시키거나 감소시키고, 그 결과를 로그로 출력합니다.
ChannelAction
클래스 (채널 변경)
class ChannelAction extends RemoteControlAction {
protected canExecute(dto: IRemoteControlRequest): boolean {
return this.context.powerOn;
}
protected onExecute(dto: IRemoteControlRequest): boolean {
if (!this.context.powerOn) {
return false;
}
if (!super.onExecute(dto)) {
return false;
}
if (dto.channel_number !== undefined) {
this.context.channel = dto.channel_number;
console.log(`Channel changed to: ${this.context.channel}`);
return true;
}
return false;
}
}
ChannelAction
클래스는 채널을 변경하는 액션을 처리합니다.canExecute
메서드는 TV가 켜져 있을 때만 실행을 허용하고,onExecute
에서는super.onExecute(dto)
를 호출하여 기본 동작을 실행한 후,channel_number
가 제공되면 채널을 변경합니다.
결론
- 이 구조는 각 TV 기능을 독립적인 액션으로 분리하고, 공통된 인터페이스(
IRemoteControlRequest
)와 상속 구조를 사용하여 코드의 유연성과 확장성을 제공합니다. - 새로운 액션을 추가하고자 할 때는
RemoteControlAction
을 상속받는 새로운 클래스를 추가하면 되며,BaseCommand
를 통해 공통된 실행 흐름을 관리할 수 있습니다.
전체코드
// TvRemoteController.ts
import BaseCommand from './BaseCommand';
interface IRemoteControlRequest {
channel_number?: number;
volume_up?: boolean;
}
class TeleVision {
powerOn: boolean;
volume: number;
channel: number;
constructor() {
this.powerOn = false;
this.volume = 0;
this.channel = 0;
}
}
// RemoteControlAction.ts (기본 액션 클래스)
class RemoteControlAction extends BaseCommand<IRemoteControlRequest, boolean, TeleVision> {
protected canExecute(dto: IRemoteControlRequest): boolean {
return true;
}
protected onExecute(dto: IRemoteControlRequest): boolean {
console.log("Action executed on TV");
return true;
}
}
// ToggleAction.ts (전원 토글 액션)
class ToggleAction extends RemoteControlAction {
protected onExecute(dto: IRemoteControlRequest): boolean {
// RemoteControlAction의 onExecute 실행 이후후
super.onExecute(dto);
this.context.powerOn = !this.context.powerOn;
console.log(`TV power is now ${this.context.powerOn ? 'ON' : 'OFF'}`);
return this.context.powerOn;
}
}
// VolumeAction.ts (볼륨 조정 액션)
class VolumeAction extends RemoteControlAction {
protected canExecute(dto: IRemoteControlRequest): boolean {
return this.context.powerOn; // 텔레비전이 켜져 있을 때만 실행
}
protected onExecute(dto: IRemoteControlRequest): boolean {
if (!this.context.powerOn) {
return false; // TV가 꺼져 있으면 볼륨 조정이 불가능하므로 false 반환
}
const isUp = dto.volume_up || false;
this.context.volume += isUp ? 1 : -1;
console.log(`Current volume: ${this.context.volume}`);
return true; // 볼륨이 변경되었으므로 true 반환
}
}
// ChannelAction.ts (채널 변경 액션)
class ChannelAction extends RemoteControlAction {
protected canExecute(dto: IRemoteControlRequest): boolean {
return this.context.powerOn; // 텔레비전이 켜져 있을 때만 실행
}
protected onExecute(dto: IRemoteControlRequest): boolean {
if (!this.context.powerOn) {
return false; // TV가 꺼져 있으면 채널 변경이 불가능하므로 false 반환
}
// 부모 클래스의 onExecute 호출
if (!super.onExecute(dto)) {
return false; // 부모 클래스가 false를 반환하면 실행되지 않음
}
if (dto.channel_number !== undefined) {
this.context.channel = dto.channel_number;
console.log(`Channel changed to: ${this.context.channel}`);
return true;
}
return false; // channel_number가 없는 경우 false 반환
}
}
export { TeleVision, RemoteControlAction, ToggleAction, VolumeAction, ChannelAction };
테스트 코드
import { TeleVision, ToggleAction, VolumeAction, ChannelAction } from "../command_pattern/TvRemoteController ";
describe('TV Remote Controller', () => {
let tv: TeleVision;
let toggleAction: ToggleAction;
let volumeAction: VolumeAction;
let channelAction: ChannelAction;
beforeEach(() => {
tv = new TeleVision(); // 새로운 TV 객체 생성
toggleAction = new ToggleAction(tv); // ToggleAction 생성
volumeAction = new VolumeAction(tv); // VolumeAction 생성
channelAction = new ChannelAction(tv); // ChannelAction 생성
});
test('should toggle TV power on and off', () => {
toggleAction.execute({}); // TV 켜기
expect(tv.powerOn).toBe(true); // TV가 켜졌는지 확인
toggleAction.execute({}); // TV 끄기
expect(tv.powerOn).toBe(false); // TV가 꺼졌는지 확인
});
test('should increase volume when TV is on', () => {
toggleAction.execute({}); // TV 켜기
expect(tv.powerOn).toBe(true); // TV가 켜졌는지 확인
volumeAction.execute({ volume_up: true }); // 볼륨 올리기
expect(tv.volume).toBe(1); // 볼륨이 1로 증가했는지 확인
});
test('should not adjust volume when TV is off', () => {
tv.powerOn = false; // TV 끄기
expect(tv.powerOn).toBe(false); // TV가 꺼졌는지 확인
const result = volumeAction.execute({ volume_up: true }); // 볼륨 올리기 시도
expect(tv.volume).toBe(0); // 볼륨은 그대로 0이어야 함
expect(result).toBe(false); // 볼륨 조정이 실패해야 함
});
test('should change channel when TV is on', () => {
toggleAction.execute({}); // TV 켜기
expect(tv.powerOn).toBe(true); // TV가 켜졌는지 확인
channelAction.execute({ channel_number: 5 }); // 채널 변경
expect(tv.channel).toBe(5); // 채널이 5로 변경되었는지 확인
});
test('should not change channel when TV is off', () => {
tv.powerOn = false; // TV 끄기
expect(tv.powerOn).toBe(false); // TV가 꺼졌는지 확인
const result = channelAction.execute({ channel_number: 10 }); // 채널 변경 시도
expect(tv.channel).toBe(0); // 채널은 변경되지 않아야 함
expect(result).toBe(false); // 채널 변경이 실패해야 함
});
test('should execute ToggleAction when TV is off', () => {
tv.powerOn = false; // TV 끄기
expect(tv.powerOn).toBe(false);
const result = toggleAction.execute({}); // TV 전원 토글
expect(tv.powerOn).toBe(true); // TV가 켜져야 함
expect(result).toBe(true); // 토글이 성공했음을 확인
});
test('should not execute any action when TV is off for VolumeAction and ChannelAction', () => {
tv.powerOn = false; // TV 끄기
expect(tv.powerOn).toBe(false);
const volumeResult = volumeAction.execute({ volume_up: true });
const channelResult = channelAction.execute({ channel_number: 5 });
expect(tv.volume).toBe(0); // 볼륨은 변경되지 않아야 함
expect(tv.channel).toBe(0); // 채널은 변경되지 않아야 함
expect(volumeResult).toBe(false); // 볼륨 조정이 실패해야 함
expect(channelResult).toBe(false); // 채널 변경이 실패해야 함
});
});
클래스 설계에서 의존성 주입과 직접 객체 생성의 차이점에 대해 배운 점
개발을 하다 보면 객체 생성
과 의존성 주입
(Dependency Injection)이라는 개념에 대해 자연스럽게 마주하게 됩니다. 처음에는 두 개념의 차이를 명확하게 구분하지 못했지만, 이번에 TvRemoteController
클래스를 설계하면서 그 차이점을 이해하고, 어떻게 해결할 수 있는지에 대해 명확히 알게 되었습니다.
1. 직접 객체 생성의 문제점
처음에는 TvRemoteController
클래스가 TeleVision
객체를 직접 생성하는 방식으로 코드를 작성했습니다. 아래와 같이 말이죠:
class TvRemoteController {
tv: TeleVision;
// TV 객체를 직접 내부에서 생성
constructor() {
this.tv = new TeleVision(); // 내부에서 TV 객체 생성
}
public PowerSwitch() {
console.log("전자기파 발산");
this.tv.powerOn = !this.tv.powerOn;
}
// ...
}
이 방식은 코드가 간단하게 작성되어 초기 개발 속도가 빨랐습니다. 그러나 시간이 지나면서 이 방식의 단점이 드러나기 시작했습니다. TvRemoteController
가 TeleVision
객체를 직접 관리하게 되어, 객체 간 결합도가 증가하고, 다른 TV 모델을 테스트하거나 바꾸려면 TvRemoteController
코드를 수정해야 하는 문제가 발생했습니다. 또한, TeleVision
객체를 외부에서 주입하지 않기 때문에 테스트 코드 작성도 어려웠습니다.
2. 의존성 주입을 통한 해결
이 문제를 해결하기 위해, TvRemoteController
클래스가 TeleVision
객체를 생성자에서 외부에서 주입받는 방식으로 변경했습니다. 이렇게 하면 TeleVision
객체의 생성 및 관리가 TvRemoteController
클래스 밖에서 이루어지므로 결합도가 낮아지고, 유연성이 증가합니다.
class TvRemoteController {
tv: TeleVision;
// 생성자로 외부에서 TV 객체를 주입받음
constructor(tv: TeleVision) {
this.tv = tv;
}
public PowerSwitch() {
console.log("전자기파 발산");
this.tv.powerOn = !this.tv.powerOn;
}
// ...
}
이제 TvRemoteController
는 TeleVision
에 의존하지만, TeleVision
객체를 외부에서 주입받기 때문에, 다양한 TV 객체를 테스트할 수 있게 되었고, 코드의 유연성도 증가했습니다. 예를 들어, TeleVision
을 모킹(mocking)하거나 다른 TV 객체로 대체할 수 있게 되어 테스트가 용이해졌습니다. 또한, TvRemoteController
클래스는 더 이상 TeleVision
의 생성에 책임지지 않으므로, 테스트 코드 작성이 쉬워졌습니다.
3. 결합도와 유연성의 차이
TvRemoteController
가 TeleVision
객체를 내부에서 직접 생성하던 방식과, 외부에서 TeleVision
을 주입받는 방식의 차이는 주로 결합도와 유연성에서 차이를 보였습니다.
- 직접 객체 생성:
TvRemoteController
는TeleVision
객체를 직접 생성하므로, 두 객체는 강하게 결합되어 있습니다. 이로 인해 객체 변경 시TvRemoteController
코드도 수정해야 하는 번거로움이 있었고, 테스트가 어려워졌습니다. - 의존성 주입:
TvRemoteController
는TeleVision
객체를 외부에서 주입받기 때문에, 두 객체 간의 결합도가 낮아졌습니다. 이 덕분에TeleVision
을 다른 객체로 교체하거나, 다양한 TV 객체를 테스트할 수 있어 코드의 유연성이 증가했습니다.
4. 배운 점
- 의존성 주입을 사용하면 코드의 유연성과 확장성이 높아지고, 테스트가 용이해집니다. 또한, 객체 간 결합도를 낮추기 때문에, 나중에 변경이 필요할 때 수정할 부분이 적어집니다.
- 직접 객체 생성은 빠르고 간단하게 작성할 수 있지만, 결합도가 높아지고, 변경이나 테스트가 어려워지기 때문에, 규모가 커지거나 복잡한 시스템에서는 적합하지 않습니다.
5. 결론
이번 경험을 통해, 클래스 설계 시 의존성 주입의 중요성을 배웠습니다. 간단한 시스템에서는 객체를 직접 생성하는 방식이 편리할 수 있지만, 유지보수나 테스트가 중요한 시스템에서는 의존성 주입을 통해 결합도를 낮추고 유연한 구조를 만들 수 있다는 점을 확실히 이해하게 되었습니다.
Database 공부
MSSQL, PostgreSQL, MYSQL 차이점
특징 | MSSQL | PostgreSQL | MySQL |
---|---|---|---|
트랜잭션 처리 | 고급 트랜잭션 처리 지원, ACID 준수 | 고급 트랜잭션 및 MVCC 지원, ACID 준수 | ACID 준수, 기본적인 트랜잭션 지원 |
쿼리 처리 | 복잡한 쿼리 및 집합 연산 처리에 강력, 최적화 기능 제공 | 고급 쿼리 기능, 복잡한 쿼리 처리 및 확장성 뛰어남 | 간단한 쿼리 최적화에 강점, 복잡한 쿼리 처리 시 성능 저하 |
데이터 처리 | 대규모 트랜잭션 및 분석 처리에 최적화 | 대규모 데이터 처리, 복잡한 데이터 모델 및 쿼리 처리 강력 | 웹 기반의 소규모 데이터 처리에 최적화 |
지연 시간 | 높은 성능, 대규모 트랜잭션 처리에서 낮은 지연 시간 | 높은 성능, 복잡한 쿼리에서 낮은 지연 시간 | 웹 애플리케이션에서 낮은 지연 시간 |
복잡한 연산 처리 | 윈도우 함수 및 CTE(공통 테이블 표현식) 지원 | 윈도우 함수, CTE 및 다양한 고급 연산 처리 지원 | 윈도우 함수 및 CTE 제한적 지원 |
데이터 무결성 | 강력한 데이터 무결성 제약 (Foreign Key, Check 등) | 고급 무결성 제약 (Foreign Key, Check 등) | 기본적인 데이터 무결성 제약 지원 |
성능 최적화 | 통합된 성능 분석 도구 및 쿼리 최적화 기능 | 다양한 인덱싱 옵션과 성능 최적화 기능 | 간단한 인덱스 및 쿼리 최적화 기능 |
관계형 데이터베이스의 특징
관계형 데이터베이스는 데이터를 행과 열로 구성된 테이블에 저장하며, 이를 관리하기 위해 SQL(구조적 쿼리 언어)을 사용합니다. 트랜잭션의 원자성, 일관성, 격리성, 지속성을 보장하는 ACID 특성을 따르며, 데이터 중복을 줄이고 효율적인 저장을 위해 정규화를 사용합니다.
기본 키, 외래 키 등으로 데이터의 정확성을 유지하고, 여러 테이블 간의 관계를 정의하여 데이터를 연결합니다. 또한, 여러 쿼리를 하나의 트랜잭션으로 처리하고 COMMIT과 ROLLBACK을 통해 관리합니다. 보안 기능을 통해 사용자 인증 및 권한 관리로 데이터 접근을 제어하며, 데이터 보호를 위해 정기적인 백업과 복구 기능도 제공합니다
SQL문 작성
1. 테이블을 생성하고 초기데이터에 맞게 데이터를 저장.
product 테이블
prod_cd(PK) | prod_nm | price | remark |
---|---|---|---|
0001 | 진라면 | 2000 | 맛있어요 |
0002 | 신라면 | 1500 | null |
0003 | 열라면 | 2500 | 너무 매워요 |
0004 | 너구리 | 3000 | null |
테이블블 생성 쿼리문
create table v5test.product_nkm (
prod_cs varchar(10) primary key,
prod_nm varchar(50) not null,
price int not null,
remark text
)
데이터 생성 쿼리문
INSERT INTO v5test.product_nkm (prod_cs, prod_nm, price, remark)
VALUES
('0001', '진라면', 2000, '맛있어요'),
('0002', '신라면', 1500, NULL),
('0003', '열라면', 2500, '너무 매워요'),
('0004', '너구리', 3000, NULL);
cust 테이블
cust_cd(PK) | cust_nm |
---|---|
0001 | 일카운트 |
0002 | 이카운트 |
0003 | 삼카운트 |
cust 테이블 생성 쿼리문
create table v5test.cust_nkm (
cust_cd varchar(10) primary key,
cust_nm varchar(50) not null
)
cust table의 데이터 삽입 쿼리문
INSERT INTO v5test.cust_nkm (cust_cd, cust_nm)
VALUES
('0001', '일카운트'),
('0002', '이카운트'),
('0003', '삼카운트');
sale 테이블
no(PK) | prod_cd | cust_cd | qty | date |
---|---|---|---|---|
1 | 0001 | 0002 | 10 | 2024-12-01 |
2 | 0002 | 0001 | 5 | 2024-12-12 |
3 | 0001 | 0001 | 15 | 2024-12-11 |
4 | 0004 | 0001 | 100 | 2024-12-07 |
5 | 0001 | 0002 | 8 | 2024-12-06 |
6 | 0002 | 0003 | 300 | 2024-12-01 |
7 | 0003 | 0003 | 1000 | 2024-12-12 |
8 | 0004 | 0002 | 16 | 2024-12-07 |
CREATE TABLE v5test.sale_nkm (
no SERIAL PRIMARY KEY, -- 고유 번호 (자동 증가)
prod_cd VARCHAR(10) NOT NULL, -- 제품 코드
cust_cd VARCHAR(10) NOT NULL, -- 고객 코드
qty INT NOT NULL, -- 수량
date DATE NOT NULL, -- 판매 날짜
FOREIGN KEY (prod_cd) REFERENCES v5test.product_nkm (prod_cs), -- 외래키
FOREIGN KEY (cust_cd) REFERENCES v5test.cust_nkm (cust_cd) -- 외래키
);
INSERT INTO v5test.sale_nkm (prod_cd, cust_cd, qty, date)
VALUES
('0001', '0002', 10, '2024-12-01'),
('0002', '0001', 5, '2024-12-12'),
('0001', '0001', 15, '2024-12-11'),
('0004', '0001', 100, '2024-12-07'),
('0001', '0002', 8, '2024-12-06'),
('0002', '0003', 300, '2024-12-01'),
('0003', '0003', 1000, '2024-12-12'),
('0004', '0002', 16, '2024-12-07');
2. 진라면의 품목 코드와 가격을 조회.
결과
prod_cd | price |
---|---|
0001 | 2000 |
select prod_cs AS prod_cd, price
from v5test.product_nkm
where prod_nm = '진라면'
3. 품목코드가 0001인 판매전표를 최신순으로 조회.
결과
no | prod_cd | cust_cd | qty | date |
---|---|---|---|---|
3 | 0001 | 0001 | 15 | 2024-12-11 |
5 | 0001 | 0002 | 8 | 2024-12-06 |
1 | 0001 | 0002 | 10 | 2024-12-01 |
select prod_cd, cust_cd, qty, date
from v5test.sale_nkm
where prod_cd = '0001'
order by date desc;
4. 날짜별 판매량을 오래된순으로 조회.
- pgSQL의 sum 함수 참고
결과
date | qty |
---|---|
2024-12-01 | 310 |
2024-12-06 | 8 |
2024-12-07 | 116 |
2024-12-11 | 15 |
2024-12-12 | 1005 |
select date, SUM(qty) AS qty
from v5test.sale_nkm
group by date
order by date ASC;
5. 판매 전표별 품목명, 회사명, 수량을 수량이 큰 순서대로 조회.
결과
no | prod_nm | cust_nm | qty |
---|---|---|---|
7 | 열라면 | 삼카운트 | 1000 |
6 | 신라면 | 삼카운트 | 300 |
4 | 너구리 | 일카운트 | 100 |
8 | 너구리 | 이카운트 | 16 |
3 | 진라면 | 일카운트 | 15 |
1 | 진라면 | 이카운트 | 10 |
5 | 진라면 | 이카운트 | 8 |
2 | 신라면 | 일카운트 | 5 |
select
s.no,
p.prod_nm,
c.cust_nm,
s.qty
from
v5test.sale_nkm s
join
v5test.product_nkm p on s.prod_cd = p.prod_cs
join
v5test.cust_nkm c on s.cust_cd = c.cust_cd
order by
s.qty desc;
JOIN 종류 및 문법 설명
JOIN 종류 | 설명 | 문법 예시 | 결과 |
---|---|---|---|
INNER JOIN | 두 테이블 간에서 공통된 조건을 만족하는 데이터만 반환. | sql SELECT * FROM table1 INNER JOIN table2 ON table1.id = table2.id; |
교집합: 두 테이블에서 공통된 조건을 만족하는 데이터만 조회됩니다. |
LEFT JOIN | 왼쪽 테이블(좌측)의 모든 데이터를 포함하고, 오른쪽 테이블에서 일치하는 데이터를 반환. 일치하지 않으면 NULL 반환. |
sql SELECT * FROM table1 LEFT JOIN table2 ON table1.id = table2.id; |
왼쪽 테이블의 모든 데이터가 포함되며, 오른쪽 테이블에 일치하는 데이터가 없으면 NULL 로 표시됩니다. |
RIGHT JOIN | 오른쪽 테이블(우측)의 모든 데이터를 포함하고, 왼쪽 테이블에서 일치하는 데이터를 반환. 일치하지 않으면 NULL 반환. |
sql SELECT * FROM table1 RIGHT JOIN table2 ON table1.id = table2.id; |
오른쪽 테이블의 모든 데이터가 포함되며, 왼쪽 테이블에 일치하는 데이터가 없으면 NULL 로 표시됩니다. |
FULL OUTER JOIN | 양쪽 테이블의 모든 데이터를 포함하며, 일치하는 데이터가 없으면 NULL 반환. |
sql SELECT * FROM table1 FULL OUTER JOIN table2 ON table1.id = table2.id; |
양쪽 테이블의 모든 데이터를 포함하며, 일치하는 데이터가 없으면 NULL 로 표시됩니다. |
CROSS JOIN | 두 테이블의 모든 가능한 조합을 반환. 보통 큰 데이터셋을 생성하는 데 사용. | sql SELECT * FROM table1 CROSS JOIN table2; |
카르티시안 곱: 두 테이블에서 가능한 모든 조합의 결과를 반환합니다. 테이블1의 행 수와 테이블2의 행 수를 곱한 만큼 결과가 반환됩니다. |
SELF JOIN | 같은 테이블을 두 번 조인하여 데이터를 비교할 때 사용. | sql SELECT A.*, B.* FROM table A, table B WHERE A.id = B.related_id; |
동일 테이블을 두 번 조인하여 데이터를 비교하는 데 사용됩니다. 예를 들어, 부모와 자식 관계를 표현하는데 유용합니다. |
Transaction
1. 트랜잭션이란?
트랜잭션(Transaction)은 데이터베이스에서 하나의 논리적 작업 단위로, 여러 명령들을 하나로 묶어 처리가 이루어지는 단위입니다. 트랜잭션은 하나의 작업으로 완전히 처리되어야 하며, 중간에 오류가 발생하면 전체 작업이 롤백(취소)되어 데이터베이스의 일관성을 유지할 수 있도록 합니다.
트랜잭션은 4가지 중요한 특성(ACID)을 가지고 있습니다:
- 원자성 (Atomicity)
- 일관성 (Consistency)
- 독립성 (Isolation)
- 지속성 (Durability)
2. 예시
도서 대출 시스템을 예로 들어 설명하겠습니다. 사용자가 도서를 대출하는 과정에서는 두 가지 작업이 필요합니다:
- 도서 재고에서 1권 차감
- 대출 기록을
borrow_records
테이블에 추가
이 두 작업은 하나의 트랜잭션으로 묶여야 하며, 중간에 하나라도 실패하면 전체 작업이 롤백되어야 합니다. 트랜잭션을 사용하면 데이터의 일관성을 보장할 수 있습니다.
BEGIN;
-- 1. 도서 재고에서 1권 차감
UPDATE books
SET stock = stock - 1
WHERE book_id = 123;
-- 2. 대출 기록 추가
INSERT INTO borrow_records (user_id, book_id, borrow_date)
VALUES (456, 123, CURRENT_DATE);
-- 트랜잭션 커밋
COMMIT;
3. 결과
- 성공적인 경우: 두 작업이 모두 성공하면 트랜잭션은
COMMIT
으로 완료되고, 변경 사항은 데이터베이스에 반영됩니다. 도서 재고가 차감되고, 대출 기록이 추가됩니다. - 실패하는 경우: 만약
INSERT INTO borrow_records
가 실패하면 트랜잭션은 롤백되고,UPDATE books
도 롤백됩니다. 즉, 도서 재고는 차감되지 않으며, 대출 기록도 추가되지 않습니다.
BEGIN;
-- 1. 도서 재고에서 1권 차감
UPDATE books
SET stock = stock - 1
WHERE book_id = 123;
-- 2. 대출 기록 추가 (예: 잘못된 user_id나 book_id가 입력된 경우)
INSERT INTO borrow_records (user_id, book_id, borrow_date)
VALUES (999, 123, CURRENT_DATE);
-- 오류가 발생하면 롤백
ROLLBACK;
4. 결과는 원자성, 일관성, 독립성, 지속성으로
원자성 (Atomicity): 트랜잭션은 "모두 또는 아무것도"라는 원칙에 따라 처리됩니다. 즉, 모든 작업이 성공하면 변경 사항이 저장되고, 하나라도 실패하면 모든 변경 사항이 취소됩니다. 예를 들어, 도서 재고를 차감하는 작업은 성공했지만 대출 기록 삽입이 실패하면 두 작업 모두 롤백됩니다.
일관성 (Consistency): 트랜잭션 전후에 데이터는 일관된 상태를 유지해야 합니다. 예를 들어, 도서 재고에서 차감된 금액은 반드시 데이터베이스에 반영되어야 하며, 대출 기록이 추가되지 않으면 트랜잭션을 롤백하여 데이터의 불일치가 발생하지 않도록 합니다.
독립성 (Isolation): 동시에 여러 트랜잭션이 실행될 때, 하나의 트랜잭션은 다른 트랜잭션의 중간 상태를 볼 수 없습니다. 예를 들어, A가 도서를 대출하고 B가 다른 도서를 대출할 때, A와 B의 트랜잭션이 서로 독립적으로 처리되어야 합니다. 서로의 트랜잭션을 방해하지 않으며, 중간 결과를 볼 수 없게 처리됩니다.
지속성 (Durability): 트랜잭션이 커밋되면 그 결과는 영구적으로 데이터베이스에 반영됩니다. 예를 들어, 도서 대출 트랜잭션이 커밋되면 도서 재고의 차감과 대출 기록의 삽입은 영구적으로 데이터베이스에 반영되며, 시스템 장애가 발생해도 데이터는 손실되지 않습니다.
Isolation Level
트랜잭션 격리 수준
격리 수준 | 설명 | 주요 문제점 | 성능 | 예제 쿼리 |
---|---|---|---|---|
Read Uncommitted | 트랜잭션이 다른 트랜잭션의 커밋되지 않은 데이터도 읽을 수 있음. | - 더티 리드(Dirty Read): 커밋되지 않은 데이터를 읽을 수 있음. | 가장 빠름, 성능 우수 | sql SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; |
Read Committed | 트랜잭션은 다른 트랜잭션이 커밋한 데이터만 읽을 수 있음. | - 비반복 가능한 읽기(Non-repeatable Read): 트랜잭션 내에서 데이터가 변경될 수 있음. | 성능 우수하지만 일부 동시성 문제 있음 | sql SET TRANSACTION ISOLATION LEVEL READ COMMITTED; |
Repeatable Read | 트랜잭션이 데이터를 읽으면, 해당 트랜잭션 내에서는 데이터가 변경되지 않음. | - 팬텀 리드(Phantom Read): 다른 트랜잭션이 데이터 삽입/삭제로 쿼리 결과가 달라질 수 있음. | 성능 저하, 높은 격리 | sql SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; |
Serializable | 모든 트랜잭션이 직렬적으로 실행되는 것처럼 처리되어, 데이터의 일관성이 보장됨. | - 성능 저하: 다른 트랜잭션이 모두 직렬적으로 실행되어 동시성 성능이 크게 저하됨. | 가장 느림, 동시성 완전 차단 | sql SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; |
예제 쿼리
1. Read Uncommitted
Read Uncommitted
에서는 커밋되지 않은 데이터를 읽을 수 있습니다. 이는 "더티 리드(Dirty Read)"를 허용합니다. 예를 들어, 한 트랜잭션이 데이터를 업데이트 중일 때 다른 트랜잭션이 해당 데이터를 읽는 것이 가능합니다.
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
-- 트랜잭션 A: 값 수정
UPDATE accounts SET balance = balance - 100 WHERE account_id = 'A';
-- 트랜잭션 B: 트랜잭션 A가 아직 커밋되지 않은 데이터 읽기
SELECT * FROM accounts WHERE account_id = 'A'; -- 더티 리드 발생 가능
COMMIT;
2. Read Committed
Read Committed
는 트랜잭션이 다른 트랜잭션에서 커밋한 데이터만 읽을 수 있게 하며, "비반복 가능한 읽기(Non-repeatable Read)"를 발생시킬 수 있습니다.
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
-- 트랜잭션 A: 첫 번째 값 읽기
SELECT balance FROM accounts WHERE account_id = 'A';
-- 트랜잭션 B: 트랜잭션 A가 읽은 데이터 수정
UPDATE accounts SET balance = balance - 50 WHERE account_id = 'A';
-- 트랜잭션 A: 같은 값을 다시 읽음, 값이 달라질 수 있음 (비반복적인 읽기 발생)
SELECT balance FROM accounts WHERE account_id = 'A';
COMMIT;
3. Repeatable Read
Repeatable Read
는 트랜잭션 내에서 읽은 데이터는 다른 트랜잭션에 의해 변경되지 않도록 보장합니다. 그러나 "팬텀 리드(Phantom Read)"가 발생할 수 있습니다.
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
-- 트랜잭션 A: 값 읽기
SELECT balance FROM accounts WHERE account_id = 'A';
-- 트랜잭션 B: 새 레코드 삽입
INSERT INTO accounts (account_id, balance) VALUES ('B', 500);
-- 트랜잭션 A: 삽입된 레코드를 포함한 쿼리 실행 (팬텀 리드 발생 가능)
SELECT * FROM accounts WHERE balance > 200;
COMMIT;
4. Serializable
Serializable
는 가장 높은 격리 수준으로, 트랜잭션이 직렬적으로 실행되도록 보장합니다. 모든 트랜잭션은 다른 트랜잭션과 겹치지 않도록 처리됩니다.
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
-- 트랜잭션 A: 값 읽기
SELECT balance FROM accounts WHERE account_id = 'A';
-- 트랜잭션 B: 트랜잭션 A가 읽은 데이터를 수정하려면 대기
UPDATE accounts SET balance = balance - 100 WHERE account_id = 'A';
COMMIT;
참고자료
wiki - Isolation_(database_systems)
'CS' 카테고리의 다른 글
[cs]깃(Git)이란? (1) | 2024.12.27 |
---|---|
[CS] Hash Dictionary - TypeScript (1) | 2024.12.20 |
[CS] TypeScript를 이해하기 위한 JavaScript 고찰(양방향 리스트를 이용하여 Stack, Queue를 Typescript로 구현하기) (2) | 2024.12.19 |
[CS] 회사 들어가기 전 공부 목록 정리하기 (1) | 2024.12.15 |
[CS] 입사 전에 회사 코드 훔쳐보기2 (1) | 2024.12.15 |