https://leetcode.com/problems/ransom-note/
주어진 ransomNote가 magazine의 글자를 이용하여 만들어질 수 있는지 확인하는 문제이다.
매거진에 있는 각 문자는 한 번만 사용할 수 있기 때문에
ransomNote의 각 문자를 magazine에서 충분히 가져올 수 있는지 검사하는 것이 핵심이다.
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
HashMap<Character, Integer> map = new HashMap<>();
int len = magazine.length();
for(int i = 0; i < len; i++) {
char current = magazine.charAt(i);
map.put(current, map.getOrDefault(current, 0) + 1);
}
len = ransomNote.length();
for(int i = 0; i < len; i++) {
char key = ransomNote.charAt(i);
if(map.get(key) != null && map.get(key) > 0) {
map.put(key, map.get(key) - 1);
} else {
return false;
}
}
return true;
}
}
이 코드는 매거진과 ransomNote의 길이를 각각 한 번씩 순회하며
각 문자의 개수를 세고 비교하므로 시간 복잡도는 O(m + n) (m: 매거진의 길이, n: 랜섬노트의 길이) 이다.
[1] 매거진의 문자 개수 세기
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
// ...
HashMap<Character, Integer> map = new HashMap<>();
int len = magazine.length();
for (int i = 0; i < magazine.length(); i++) {
char current = magazine.charAt(i);
map.put(current, map.getOrDefault(current, 0) + 1);
}
// ...
}
}
여기서 HashMap을 사용하여 매거진에 등장하는 각 문자의 개수를 기록한다.
map.getOrDefault(current, 0) + 1
은 현재 문자가 map에 이미 존재하면 그 값을 1 증가시키고, 그렇지 않으면 0을 기본값으로 설정하여 1로 초기화한다.
[2] ransomNote에서 각 문자를 매거진에서 찾기
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
// ...
len = ransomNote.length();
for(int i = 0; i < ransomNote.length(); i++) {
char key = ransomNote.charAt(i);
if(map.get(key) != null && map.get(key) > 0) {
map.put(key, map.get(key) - 1);
} else {
return false;
}
}
// ...
}
}
이 반복문에서는 ransomNote에 있는 각 문자를 하나씩 확인한다.
map.get(key) != null && map.get(key) > 0
은 매거진에 해당 문자가 존재하고, 아직 사용 가능한 문자의 개수가 남아 있는지 확인한다.
해당 문자가 매거진에 있다면 map.put(key, map.get(key) - 1)
로 해당 문자의 개수를 1 감소한다.
만약 문자가 더 이상 없거나 매거진에 존재하지 않으면 즉시 false
를 반환하여 ransomNote를 만들 수 없음을 알린다.
import java.util.HashMap;
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
// 1. HashMap을 이용해 magazine의 각 문자의 개수 기록
HashMap<Character, Integer> map = new HashMap<>();
for (int i = 0; i < magazine.length(); i++) {
char current = magazine.charAt(i);
map.put(current, map.getOrDefault(current, 0) + 1);
}
// 2. ransomNote의 각 문자를 검사
for (int i = 0; i < ransomNote.length(); i++) {
char current = ransomNote.charAt(i);
// map에 문자가 없거나, 개수가 부족하면 false 반환
if (!map.containsKey(current) || map.get(current) == 0) {
return false;
}
// 사용한 문자의 개수를 줄임
map.put(current, map.get(current) - 1);
}
// 3. 모든 문자를 확인한 후 true 반환
return true;
}
}
[1] magazine의 문자와 등장 횟수를 HashMap에 기록
매거진의 각 문자를 순회하면서, 각각의 문자가 몇 번 등장하는지를 HashMap에 기록한다.
이때, charAt() 메서드를 사용하여 magazine의 각 문자에 접근한다.
[2] ransomNote의 각 문자를 HashMap에서 확인
ransomNote의 각 문자를 확인하며, 그 문자가 HashMap에서 충분히 있는지 확인한다.
HashMap에서 문자가 있는지 확인하고, 있다면 그 문자의 개수를 줄여준다.
만약 문자가 없거나 충분하지 않다면, 바로 false를 반환한다.
HashMap의 개념과 문법을 알아야 문제를 쉽게 풀 수 있다.
첫 번째 풀이가 Runtime 시간이 10ms로 두 번째 풀이보다 6ms 더 빠르다.
if(map.get(key) != null && map.get(key) > 0)
https://leetcode.com/problems/implement-queue-using-stacks/description/
두 개의 스택을 사용해 큐(FIFO)를 구현하는 문제였다.
스택의 기본 연산만 사용하여 큐를 구현하라는 것이다.
큐에서의 주요 연산인 push(), pop(), peek(), empty()를 구현해야 한다.
class MyQueue {
Stack<Integer> s1;
Stack<Integer> s2;
public MyQueue() {
s1 = new Stack<>();
s2 = new Stack<>();
}
// 큐의 뒤쪽에 요소를 추가하는 연산
public void push(int x) {
s1.push(x);
}
// 큐의 앞쪽 요소를 제거하는 연산
public int pop() {
// s2가 비어 있을 때 s1의 모든 요소를 s2로 옮긴 후 pop
if (s2.isEmpty()) {
while (!s1.isEmpty()) {
s2.push(s1.pop());
}
}
return s2.pop();
}
// 큐의 앞쪽 요소를 확인하는 연산
public int peek() {
// s2가 비어 있을 때 s1의 모든 요소를 s2로 옮긴 후 peek
if (s2.isEmpty()) {
while (!s1.isEmpty()) {
s2.push(s1.pop());
}
}
return s2.peek();
}
// 큐가 비어 있는지 확인하는 연산
public boolean empty() {
return s1.isEmpty() && s2.isEmpty();
}
}
스택은 LIFO 방식이지만, 큐는 FIFO 방식을 요구합니다. 이를 해결하기 위해 두 개의 스택을 사용할 수 있다.
스택1(s1): 새로운 요소를 추가하는 스택.
스택2(s2): 요소를 제거하거나 앞쪽을 확인할 때 사용하는 스택.
이 두 스택을 사용해 큐의 동작을 흉내내면 된다.
s1에 새 요소를 계속 추가하고,
요소를 제거하거나 볼 때는 s1에서 s2로 요소를 옮기고, s2의 상단에서 요소를 제거하거나 확인할 수 있다.
이렇게 하면 FIFO 동작을 구현할 수 있다. (그림으로 확인하면 더 이해하기 쉽다 !)
2개의 스택을 이용하여 큐를 구현하는 문제였다.
구현에 대한 아이디어가 한 번에 떠오르지 못해서, 힌트를 보고 30분 안으로 구현해서 성공했다. 동작 원리는 알고 있었지만, 막상 구현하기엔 쉽지 않았다. 이 문제는 일주일 내로 다시 풀어보기로 하자 !
그리고 머릿속으로 정확히 안떠오르면 아이패드로 그림을 그려가면서 해보자. 그럼 더 쉬울 거다 !
책의 내용 중 자극히 개인적으로 중요한, 기억하고 싶은 부분들을 메모해두자. 자세한 내용은 책을 참고하자.
실제 서비스되는 환경에서 실행되는 소프트웨어가 되기까지의 과정
코드를 작성할 때 다음과 같은 네 가지 상위 수준의 목표를 달성해야 한다.
코드 품질
의 여섯 가지 핵심 요소는 다음과 같다.
단일 함수 내에서 너무 많은 작업을 수행하면 코드를 이해하기 어렵게 만드는 문제가 발생할 수 있다.
그러한 문제가 발생한다면 함수를 작게 만들고 수행하는 작업을 명확하게 해서 코드의 가독성과 재사용성을 높힌다.
줄 수
(number of lines): ‘한 클래스는 코드 300 줄을 넘기지 않아야 한다’와 같은 가이드라인을 접하는 경우가 있는데, 이는 경고의 역할만 할 뿐, 본인이 속한 팀의 문화에 따라 다를 수 있다.
응집력
(cohesion): 한 클래스 내의 모든 요소들이 얼마나 잘 속해 있는지를 보여주는 척도로, 좋은 클래스는 응집력이 매우 강하다. 응집력에는 여러 방식이 있다.
순차적 응집력
이 있고, 케이크를 만들기 위해 필요한 장비를 모이는 과정과 같은 기능적 응집력
이 있다.관심사의 분리
(seperation of concerns): 시스템이 각각 별개의 문제를 다루는 개별 구성 요소로 분리되어야 한다고 주장하는 설계 원칙이다.
코드를 적절한 크기의 클래스로 쪼개지 않으면 너무 많은 개념을 한꺼번에 다루고,
실제 운영 환경에서는 ddl-auto
설정을 validate
또는 none
으로 사용하는 것이 일반적입니다. validate
는 Hibernate가 엔티티와 데이터베이스 스키마 간의 일관성을 검증하지만, 스키마를 변경하지 않습니다. 반면, 현재 개발 환경에서는 update
로 설정하여 엔티티의 변경 사항을 자동으로 스키마에 반영하고 있습니다.
하지만 이 update
방식에는 치명적인 단점이 있습니다. 엔티티에 새로운 필드를 추가하면 해당 필드가 자동으로 스키마에 반영되지만, 기존 데이터는 이 필드에 값이 없으므로 NULL 값이 들어가게 됩니다. 이로 인해 데이터 무결성 문제가 발생할 수 있습니다. 또한, 외래 키나 제약 조건이 변경될 경우 참조 무결성이 깨져 운영 환경에서 심각한 데이터 손실 및 오류를 초래할 위험이 있습니다.
이러한 문제는 운영 환경에서 더욱 심각해집니다. 로컬 개발 환경이나 테스트 서버에서는 ddl-auto
설정을 create
, create-drop
, update
로 설정하여 엔티티 구조 변경 시 스키마를 자동으로 반영할 수 있지만, 프로덕션 환경에서는 이러한 설정을 사용할 수 없습니다. 프로덕션 서버에서 테이블을 DROP하는 것은 실제 사용자 데이터가 손실될 수 있기 때문에 불가능합니다.
기존 테이블에 새로운 컬럼을 추가하거나 수정할 때는 ALTER TABLE
명령어를 사용합니다. 또한, 새로운 필드에 초기값을 설정해야 하는 경우 데이터 마이그레이션 스크립트를 작성하여 기존 데이터를 새로운 스키마에 맞게 변환해야 합니다.
ALTER TABLE member ADD COLUMN cellphone VARCHAR(255) NOT NULL;
Flyway
와 같은 마이그레이션 도구를 활용해 이러한 문제를 해결하는 것이 효과적 입니다.Flyway
는 데이터베이스 스키마 버전 관리 및 마이그레이션을 자동화하는 도구로, 주로 SQL 스크립트나 Java 코드로 작성된 스키마 변경을 관리하고 적용하는 데 사용됩니다. Flyway의 동작 원리는 다음과 같은 절차로 이루어집니다.flyway_schema_history
테이블을 찾습니다.(비어있는 데이터베이스에 Flyway가 연결된 후 flyway_schema_history 테이블을 생성한 모습입니다.)
/resources/db/migration
디렉토리에서 SQL 또는 Java로 작성된 마이그레이션 스크립트를 스캔합니다.Flyway는 작은 버전 번호부터 큰 버전 번호 순서대로 마이그레이션을 실행합니다. 예를 들어, 마이그레이션 파일이
V1__init.sql
, V2__add_table.sql
, V3__add_column.sql
형태로 존재한다면 Flyway는 이 순서대로 파일을 실행합니다.
각 마이그레이션이 성공적으로 완료되면 해당 내역이 flyway_schema_history
테이블에 기록됩니다.
Flyway는 3가지 종류의 마이그레이션을 제공합니다:
Versioned Migrations
Flyway의 핵심 기능입니다. 마이그레이션 스크립트는 버전 번호에 따라 실행되며, 데이터베이스의 현재 스키마 버전과 비교하여 차이점이 있을 경우 순차적으로 마이그레이션을 수행합니다.
예를 들어, 현재 데이터베이스가 버전 5이고 마이그레이션 파일이 버전 9까지 있다면, Flyway는 6~9 버전의 스크립트를 순차적으로 실행합니다.
Undo Migrations (유료 기능)
Repeatable Migrations
예를 들어 member 테이블에 대한 JPA 엔티티는 아래와 같이 구성됩니다.
@Table(name = "member")
@Entity
public class Member extends BaseTime {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "email", nullable = false)
private String email;
}
Spring Boot는 기본적으로 Flyway 같은 고수준의 데이터베이스 마이그레이션 도구를 지원합니다. 이러한 도구들은 데이터베이스 스키마를 효과적으로 관리하고 자동으로 초기화할 수 있게 도와줍니다. (참고)
저는 현재 서비스 환경에서 Docker를 사용하고 있지만, 반드시 Docker를 사용할 필요는 없습니다. Docker를 통해 MySQL 컨테이너를 띄우려면 아래와 같은 명령어를 사용할 수 있습니다.
docker run --name test -e MYSQL_ROOT_PASSWORD=1234 -d mysql:8.0.32
위 명령어로 MySQL 8.0.32 버전을 실행하면 컨테이너가 생성되고, 다음과 같은 상태를 확인할 수 있습니다.
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
57c250407728 mysql:8.0.32 "docker-entrypoint.s…" 5 weeks ago Up 5 weeks 0.0.0.0:3306->3306/tcp test
Flyway를 프로젝트에 적용하기 위해 build.gradle
파일에 Flyway와 MySQL 의존성을 추가합니다.
dependencies {
// flyway
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-mysql' // MySQL 8.X 버전이거나, MariaDB를 사용하는 경우
}
Flyway를 제대로 사용하려면 Spring Boot의 application-{}.yml
파일에 다음과 같이 설정을 추가해야 합니다.
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://{Private IP}:3306/test
username: root
password: 1234
jpa:
# ...
hibernate:
ddl-auto: validate # Hibernate가 스키마 검증만 수행
defer-datasource-initialization: false # Flyway 마이그레이션 후 JPA 초기화
flyway:
enabled: true # Flyway 활성화
baseline-on-migrate: true # 기존 스키마가 있을 때 기준을 설정
locations: classpath:db/migration # 마이그레이션 파일 경로
기존 데이터베이스에 Flyway를 도입하는 경우 baseline-on-migrate: true
옵션이 중요합니다. 이 옵션은 Flyway가 마이그레이션 기록을 관리하는 테이블(flyway_schema_history
)을 처음으로 생성할 때 사용됩니다.
저의 경우, 프로젝트를 진행하는 중간에 flyway
를 도입하기 때문에 해당 옵션을 true로 설정했습니다.
마이그레이션 스크립트는 resources/db/migration 디렉토리에 저장되며, 파일명은 V1__init.sql
처럼 버전과 설명을 포함합니다. 첫 번째 마이그레이션 파일을 다음과 같이 작성합니다.
파일 경로는 resources/db/migration
이며 파일명은 V1__init.sql
로 생성해주세요.
-- member 테이블 생성
CREATE TABLE IF NOT EXISTS member (
id BIGINT AUTO_INCREMENT,
email VARCHAR(255) NOT NULL,
created_at DATETIME(6) NOT NULL,
updated_at DATETIME(6) NOT NULL,
PRIMARY KEY (id)
);
Flyway는 스크립트를 순차적으로 실행하므로, 마이그레이션 파일명은 버전을 포함해야 하며, 파일 이름에서 언더스코어(_)를 두 개 사용하는 규칙을 지켜야 합니다.
애플리케이션을 실행하면 Flyway
가 정상적으로 마이그레이션 스크립트를 실행하고, 스키마 변경이 적용됩니다.
로그에서 Flyway의 실행 상태를 확인할 수 있습니다.
INFO org.flywaydb.core.internal.command.DbMigrate - Current version of schema test: 1
INFO org.flywaydb.core.internal.command.DbMigrate - Schema test is up to date. No migration necessary.
Flyway의 마이그레이션 스크립트는 버전 순서대로 실행됩니다. 숫자가 작은 파일부터 순서대로 실행되며, 숫자가 동일한 경우 파일명이 알파벳순으로 실행됩니다. 버전 관리가 중요하므로 명명 규칙을 지키는 것이 중요합니다.
버전은 정수로 처리되므로 V3.10과 V3.2 중 V3.2가 먼저 실행될 수 있음을 주의해야 합니다.
Flyway를 사용할 때는 Spring Boot의 schema.sql
과 data.sql
을 사용해 데이터베이스를 초기화하지 않아야 합니다. Flyway가 스키마를 관리하므로, 수동으로 스키마를 정의하는 것은 오류를 일으킬 수 있습니다.
또한, 새로운 엔티티가 추가될 때마다 마이그레이션 스크립트를 작성하는 습관을 들이는 것이 중요합니다. 일반적으로 프로덕션 환경에서는 ddl-auto
를 validate
로 설정하여 JPA가 자동으로 스키마를 수정하지 못하도록 합니다.
(참고)
기존 스키마에 Flyway를 도입할 때 flyway_schema_history 테이블이 없으면 오류가 발생할 수 있습니다.
에러 내용
Error creating bean with name 'flywayInitializer': Invocation of init method failed; nested exception is org.flywaydb.core.api.FlywayException: Found non-empty schema(s) test but no schema history table.
이 에러는 Flyway가 데이터베이스에 접속했을 때, 이미 존재하는 비어 있지 않은 스키마를 발견했지만, Flyway의 스키마 히스토리 테이블(flyway_schema_history
)이 없어서 발생한 문제입니다. Flyway는 마이그레이션 이력을 추적하기 위해 이 히스토리 테이블을 사용합니다. 따라서 이미 데이터가 있는 기존 데이터베이스에 Flyway를 처음 적용하려고 할 때 이러한 문제가 발생할 수 있습니다.
해결방안
기존 데이터베이스에 Flyway를 적용할 때, Flyway가 기존의 데이터베이스 스키마를 무시하고 히스토리 테이블을 생성하도록 해야 합니다. 이 작업을 위해 Flyway의 baselineOnMigrate: true
설정을 사용하면 됩니다.
이 설정을 추가하면, Flyway는 기존에 비어 있지 않은 데이터베이스에서 첫 마이그레이션을 실행할 때 히스토리 테이블을 생성하고 이를 기준으로 마이그레이션을 진행합니다.
application.{dev/local}.yml에 추가
spring:
flyway:
enabled: true
baselineOnMigrate: true # 이 옵션은 기존 데이터베이스가 있고, 그 위에 처음으로 Flyway를 적용할 때 사용됨
locations: classpath:db/migration
새로운 필드를 엔티티에 추가했지만, 이를 반영한 마이그레이션 스크립트를 작성하지 않으면 JPA에서 에러가 발생할 수 있습니다.
Member 엔티티 클래스 - cellphone 컬럼 추가
@Table(name = "member")
@Entity
public class Member extends BaseTime {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "email", nullable = false)
private String email;
@Column(name = "cellphone")
private String cellphone; // 추가된 필드
}
에러 내용은 아래와 같습니다.
Schema-validation: missing column [cellphone] in table [member]
이 문제를 해결하기 위해 새로운 마이그레이션 스크립트를 생성해야 합니다. 예를 들어 V2__member_add_cellphone.sql
파일을 생성하고, 다음과 같이 컬럼을 추가합니다.
예를 들어, V2__member_add_cellphone.sql
파일을 생성하고, 다음과 같이 컬럼을 추가합니다.
ALTER TABLE member ADD COLUMN cellphone VARCHAR(255);
이 스크립트는 기존 테이블에 새로운 cellphone 컬럼을 추가합니다. 파일을 적용하고 애플리케이션을 다시 실행하면, 스키마 버전이 업데이트됩니다.
마이그레이션 적용 로그 예시
해당 파일을 적용한 뒤, 애플리케이션을 다시 실행하면 아래와 같이 Version이 1 에서 2로 바뀐 것을 아래 로그를 통해 확인할 수 있습니다.
Migrating schema `{데이터베이스 내의 스키마 이름}` to version "2 - member add cellphone"
위와 같이 데이터베이스를 확인해보면, 새로운 테이블이 하나 생성된 것을 확인할 수 있습니다. flyway_schema_history
라는 테이블을 통해 Flyway는 마이그레이션에 대한 버전 관리를 합니다.
이미 적용된 마이그레이션 파일을 수정하는 것은 매우 위험할 수 있습니다. Flyway는 각 마이그레이션 파일의 체크섬(무결성)을 관리하므로, 파일을 수정하면 아래와 같은 오류가 발생할 수 있습니다.
예시:
V1__init.sql
파일에 cellphone 컬럼을 추가하려고 하면
-- member 테이블 생성
CREATE TABLE IF NOT EXISTS member (
id BIGINT AUTO_INCREMENT,
email VARCHAR(255) NOT NULL,
cellphone VARCHAR(255) NOT NULL, // // 새로운 필드 추가
created_date_time DATETIME(6) NOT NULL,
updated_date_time DATETIME(6) NOT NULL,
PRIMARY KEY (id)
);
에러 메시지는 아래와 같습니다.
ERROR org.springframework.boot.SpringApplication - Application run failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'flywayInitializer' defined in class path resource [org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration$FlywayConfiguration.class]: Invocation of init method failed; nested exception is org.flywaydb.core.api.exception.FlywayValidateException: Validate failed: Migrations have failed validation
Migration checksum mismatch for migration version 1
-> Applied to database : 1825070444
-> Resolved locally : 702378192
Either revert the changes to the migration, or run repair to update the schema history.
Need more flexibility with validation rules? Learn more: https://rd.gt/3AbJUZE
이 오류는 Flyway가 이미 적용된 마이그레이션 파일을 다시 체크했을 때, 원본 파일과 변경된 파일의 체크섬이 달라지기 때문에 발생합니다.
따라서, 스키마에 대한 모든 변경은 반드시 새로운 버전의 마이그레이션 스크립트를 추가하는 방식으로 처리해야 합니다. 이미 적용된 마이그레이션 파일을 수정하지 않고, 새로운 파일을 생성하여 변경 사항을 적용하는 것이 올바른 방법입니다. (-> V2__member_add_cellphone.sql
와 같이 새로운 버전의 마이그레이션 스크립트를 추가해야 합니다)
실무에서 리눅스 환경의 shell script 를 보면서 새롭게 알게된 /dev/null 2>&1
에 대해 정리하고자 한다.
/dev/null 2>&1
에 대해 알기 전에, 그 전에 리다이렉션, 파일 디스크립터에 대해 간단히 정리해보자.
리다이렉션
(입출력 재지정)이란 일반적인 키보드 입력, 화면 출력을 사용하지 않고 사용자가 직접 입력과 출력을 재지정한다는 의미이다.
기본적으로 표준 입력은 키보드에서 직접 받아오고, 표준 출력은 화면에 출력된다.
이러한 기본적인 하드웨어적인 입출력 동작을 변경하는 것이다.
그래서 키보드에서 입력을 받지 않고 파일 내용을 입력으로 받거나, 내용을 화면에 출력하지 않고 파일에 저장하는 것이다.
리다이렉션
에 대한 기호, 방향, 의미를 정리하면 아래와 같다.
리다이렉션 기호 , 방향 , 의미
>
, 표준 출력 , 명령 > 파일
: 명령의 결과를 파일로 저장
>>
, 표준 출력(추가) , 명령 >> 파일
: 명령의 결과를 기존 파일 데이터에 추가
<
, 표준 입력 , 명령 < 파일
: 파일의 데이터를 명령에 입력
파일 디스크립터
는 시스템에서 할당 받은 파일을 대표하는 0이 아닌 정수 값으로, 쉘 이나 시스템 프로그래밍에서 자주 접할 수 있다.
즉, 음수가 아닌 0과 양수인 정수 값을 가진다.
유닉스 시스템은 일반적인 파일부터 디렉토리, 소켓, 파이프 등 모든 객체들을 파일로 관리하는데, 쉘은 작업 중 필요한 파일에 일련번호를 붙여서 관리한다.
이 일련번호가 파일 디스크립터
이며, 표준 입출력 장치도 이 일련번호로 제어된다.
기본적으로 3가지(input, output, error)가 있다.
표준 입력(Standard Input, STDIN): 명령어에 입력될 내용을 저장 / 일련번호: 0
표준 출력(Standard Output, STDOUT): 명령어에서 출력될 내용을 저장 / 일련번호: 1
표준 오류(Standard Error, STDERR) : 명령어에서서 출력될 에러 메시지를 저장 / 일련번호: 2
그래서 사용자는 이 일련번호
를 이용하여 입출력 장치를 리다이렉션할 수 있는 것이다.
위에서 설명한 리다이렉션의 기호와 파일 디스크립터
를 가져와서 해석해보면 다음과 같다.
$ 2>&1
2>&1
는 표준 에러를 표준 출력(&1) 으로 리다이렉션(재지정)한다는 의미이다.
위의 명령어를 제대로 알기 전에, 몇 가지 예제로 정리해보자.
정상적이지 않은 명령어인 경우
$ ls -ERR 2> log.txt
ERR
라는 리눅스에 존재하지 않는 옵션을 주면, 에러가 발생하게 되는데, 이러한 에러 메시지를 화면에 출력하는게 아니라 log.txt
라는 파일에 저장하는 것이다.
이렇게 작성하면, 만일 프로그램이 에러가 나더라도 화면에 에러 메시지가 나오지 않고, 파일에 기록되어서 나중에 확인할 수 있다는 점이다.
정상적인 명령어인 경우
$ ls -al 2> log.txt
정상적인 명령어를 입력한 경우에는 조회해보면 아무런 내용이 들어가 있지 않다.
즉, 2>
표준 에러에 대한 리다이렉션은 에러 메시지만 취급하기 때문에 정상적인 명령에 대해서 아무런 작업을 하지 않는 것이다.
명령어가 성공해도 파일에 내용을 넣고, 실패해도 파일에 내용을 넣고 싶은경우 -> 한 번에 기록하고 싶다면 어떻게 해야할까?
$ ls > log.txt 2> log.txt
# 만일 명령어를 실행해서 성공하면 표준 출력 재지정을 통해 파일에 기록하고
# 명령어가 실패하면 표준 에러 재지정을 통해 파일에 기록하도록 조정
명령어가 성공하면 결과 내용이 파일에 기록되고, 명령어가 실패해도 에러 메시지가 파일에 기록될 수 있다.
여기서 2>&1
은 위의 명령어를 함축한 버전이라고 보면 된다.
$ ls > log.txt 2> log.txt #이전 버전
$ ls > log.txt 2>&1 #함축한 버전
&
: &1
에서 &
부분은 숫자 1을 표준 출력 일련번호로 인식하게 해주는 설정이다. 만일 &
기호를 안쓰게 되면 파일로 읽혀 버리기 때문이다.
2>
: 표준 에러를 표준 출력(&1)으로 재지정한다는 의미이다.
$ nohup.out > /dev/null 2>&1
여기서 /dev/null
는 블랙홀이라고 이해하면 된다.
이 경로에 보내지는 모든 파일과 데이터들이 블랙홀에 빨려들어가 (없을)무가 된다.
즉, /dev/null
로 결과를 보내는 것은 데이터를 모두 지워서 화면에 표시하지 않는다는 것을 말한다.
따라서 출력이 필요 없는 경우 > /dev/null
로 리다이렉션 시키면 된다.
결론적으로 > /dev/null 2>&1
은 표준 출력과 표준 에러를 모두 지우겠다는 의미이다.
글또는 글 쓰는 개발자 모임이며, 이번 9기에 활동했던 인원은 약 450명일 정도로 다양한 개발자 모임들 중에 큰 규모로 운영중인 모임이지 않을까 싶어요.
글또의 비전이 글을 작성하는 개발 직군분들이 모여서, 좋은 영향을 주고 서로 같이 자랄 수 있는 커뮤니티인 만큼 저도 이번 9기에 참여하면서 정말로 다양한 경험과 배움을 얻었어요.
글또에 알기 전에는 그동안 혼자서 공부한 것들을 제 개인 기술 블로그에 정리하면서 지냈던 것 같아요.
그렇게 약 2년이 지났을 때쯤, 2023년 11월부터는 서서히 제 글 쓰는 능력에 대한 부족함을 느꼈고, 그러한 점들을 어떻게 하면 개선해 나갈 수 있을까를 고민하던 찰나에, 어떤 개인 기술 블로그에 나와있는 내용을 통해 글또를 알게 되었어요.
운이 좋게도 저의 지원서를 좋게 봐주셔서 9기에 활동할 수 있게 되었고, 그렇게 2023년 12월부터 4월까지 5개월간 참여하게 되면서 제가 느꼈던 점들을 하나씩 정리해보려고 해요.
글또에 대한 궁금한 점들은 글또 소개 페이지에서 확인해주시면 될 것 같아요 ! (기존에는 Notion
으로 되어져있었는데, 이번 9기에서 글또 운영진분들의 열정과 노력 덕분에 홈페이지
로 개선되었어요)
글또를 5개월간 활동하면서, 필수적으로 지켜야 할 점은 2주에 1개의 글을 작성한 뒤에 슬랙을 통해 글을 제출하는 형식이었어요.
글또를 막 시작했을 12월에는 ‘2주에 한번 글을 쓰고 제출하는 거면 충분히 할 수 있겠네 !’ 라고 다짐했었는데, 글또 9기를 활동하는 중간에 스타트업 회사로 옮기면서, 그 다짐을 지키지 못했어요 🥲.
그래서 총 10회 차에 7번 제출, 2번 패스를 사용했고, 마지막 주차에는 제출하지 못했어요.
지금 돌이켜 생각해보니, 제출하는 글 내용에 대한 퀄리티에 제 스스로 너무 생각을 많이 했던 것 같아요.
초반에 3개월 동안(2023.12 ~ 2024.02)은 글 하나를 작성하는데 평균 10-12시간 정도 걸렸던 것 같아요. (예시 [2023년 회고] 다양한 활동으로 가득한 특별한 한 해 - 글 작성하는데 걸린 시간: 11H) 후반(나머지 2개월)에는 새로운 회사에 대한 적응하는 것과 글 쓰는 것에 대한 부담감, 그 외에 다른 여러 가지 일들로 인해 글 제출을 많이 못했어요.
이미 지나가버린 과거이지만, 다음에는 적당한 분량과 시간을 조절하면서 글을 작성하려고 해요 ✨. 무엇보다도 퀄리티에 신경써서 제출을 아예 안하는 것보다 조금이라도 작성해서 제출하는게 더 낫다는게 제 생각이에요.
글또에는 글 작성하는 게 필수이지만, 그 외에 다양한 활동은 본인의 의지로 할 수 있어요!
대표적으로 저는 아래와 같은 활동들을 했어요.
백엔드/인프라 빌리지 반상회
에 준비위원회로 참여 - ‘글또’ 키캡 제작, 음식 선정 및 주문커피챗
- (온/오프라인) 총 4
번 참여모닝또
- 기상 시간과 함께 오늘 할일을 공유하는 채널몸또
- 운동과 관련된 걸 기록하고 공유하는 채널커또
- 커피를 좋아하는 사람들이 모여서 카페 추천이나 커피 원두를 공유하는 채널월간-메이커스
- 한달 동안 1인 개발자로 본인이 원하는 주제에 맞는 사이드 프로젝트를 기획부터 개발해서 공유하는 채널 / 2
개의 사이드 프로젝트 개발TIL
- 오늘 하루에 IT
와 관련된 기술/협업에 대한 걸 정리하고 공유하는 스터디 / 2023.12 ~ 현재 진행중포폴-이력또
- 한달 간의 이력서와 포트폴리오를 다듬으면서 다양한 직무간의 사람들이 서로 피드백하는 스터디(백엔드/인프라 빌리지 반상회
, TIL
, 포폴-이력또
에 대한 자세한 후기는 2024년 1분기 회고에 작성해두었어요)
커피챗
은 회사 도메인/직무/커리어 목표/취미 등 다양한 주제로 대화를 나누고 있어요.
오프라인으로 커피챗을 하면서, ‘본인의 삶과 자기개발에 열정적이고, 재밌게 사시는 분들이 많구나‘를 느꼈어요. 그리고 커피챗에서 만난 분들이 성격과 마인드가 다 좋으셔서 ‘어떻게 다 이런 분들만 있으시지?’ 라는 생각에 많이 놀랐어요.
그렇다고 전부 다 열정적인 분들은 아니니, 너무 부담갖지 않으셔도 되세요. 오히려 그러한 분들이 있으셔서 저도 많은 자극과 동기부여를 받았고, 덕분에 긍정적인 에너지를 많이 받으면서 잘 지내고 있습니다 😊.
실제로 저는 포폴-이력또
를 통해 AI 스타트업 회사에 백엔드 개발 직무로 합격해서 현재 다니고 있어요.
해당 스터디를 운영해주신 YH님께 이 자리를 빌어 감사드리고, 그 외에 저의 이력서에 피드백을 해주신 스터디원 분들, 그리고 JY님, NY님께도 감사의 말씀 전합니다.
(포폴-이력또
에 대한 진행 과정이 궁금하시다면, YH님이 개인 블로그에 올려주신 이력또 : 한달 간의 이력서 정리 스터디 진행 후기에 참고해 주시면 감사하겠습니다)
최근에는 새로운 회사의 업무에 집중하느라 TIL
스터디 하나만 참여하고 있지만, 나중에 어느 정도 시간이 된다면 다른 스터디 혹은 취미 활동에 참여하려고 해요.
글또에 대한 아쉬운 점 보다는 제 자신에 대한 아쉬운 점이 있어요.
먼저, 위에서 언급한 것처럼 글 제출에 대한 부분이에요. 회사에 업무도 중요하지만, 하루에 몰아서 글을 작성하는 것보다는 매일 10분이라도 조금씩 글을 작성하면서 마감 기한에 맞춰서 글을 제출할 거에요. 그래서 왠만하면 패스 없이 글을 제출하는 것이 목표입니다.
그리고 월간-메이커스
와 같이 사이드 프로젝트에 대한 개발 부분이에요.
12-1월에 혼자서 사이드 프로젝트 2개를 개발부터 배포를 했지만, 디테일한 부분과 수익성 부분이 아쉬웠어요.
만약 10기에도 월간-메이커스
와 같은 채널이 있다면, 개발에 대한 기술 뿐만 아니라 비즈니스 부분도 생각하면서 서비스 출시를 목표로 할 생각입니다.
월간-메이커스
채널을 만들어주시고 운영해주신 다코(daco)님께 감사의 말씀 전합니다. (TIL
스터디 오래오래 같이 열심히 해봐요 🙌🏻)
(다코님의 블로그 제목은 코드로 우주평화 입니다)
마지막으로 글또에 조금이나라 기여를 하고자 해요.
글또 9기에는 백엔드/인프라 빌리지 반상회
에 준비위원회로 참여했지만, 반상회가 아닌 기술적으로 도움을 드리고 싶어요.
아직 무엇을 도움을 드리고자 할지에 대해서는 더 생각을 해봐야겠지만, 취업준비생분들을 위한 온/오프라인 커뮤니티 모임을 개발로 만들면 어떨까 싶어요.
저도 작년까지만 해도 취업준비생 시절을 보내왔지만, 생각보다 현업 개발자분들을 만나기가 쉽지 않았기 때문에, 이러한 커뮤니티 모임이 있으면 좋을 것 같다는 생각이 들었습니다.
위의 아쉬운 점들을 요약하면 아래와 같아요.
제가 생각하기에 글또
라는 모임은 단순히 글을 작성하는 스킬 향상에 그치지 않고, 그 외 다양한 활동이 있고 그러한 점들을 통해 개인적인 성장 뿐만 아니라 서로가 도와주면서 함께 성장하는 분위기 같아요.
시간적 여유가 되신다면 다음 글또 10기에 참여하시는 걸 적극 추천드립니다.
(위에는 예시이고, 실제로 이 밖에도 다양한 활동들이 있습니다)
지금까지 이 글을 읽어주셔서 감사합니다 😌.
글 작성하는데 걸린 시간: 3시간