claude -c --model google/gemma-4-26b-a4b-qat Claude Code 세션과 Context: 폴더를 오가며 작업할 때 알아야 할 것들 Claude Code를 쓰다 보면 이런 궁금증이 생깁니다. 프로젝트A에서 작업하다 종료하고 프로젝트B에서 작업한 뒤 다시 프로젝트A로 돌아오면, 이전 대화 맥락(context)은 남아 있을까요? 이 글에서는 세션이 어떻게 관리되는지 정리합니다. 1. 결론부터: 그냥 claude 를 실행하면 새 세션이다 프로젝트A에서 claude 를 실행해 작업하고 종료한 뒤, 프로젝트B를 거쳐 다시 프로젝트A에서 claude 를 실행하면 이전 대화 context는 이어지지 않습니다. 빈 새 세션이 열리고, Claude는 직전에 무엇을 했는지 알지 못합니다. 다만 이전 대화가 사라진 것은 아닙니다. 별도로 저장되어 있어서 필요할 때 불러올 수 있습니다. 2. 대화는 어디에 저장되는가 대화 기록은 홈 디렉터리의 ~/.claude/projects/ 아래에 프로젝트별로 저장됩니다. ~/.claude/projects/ ├── -Users-me-projectA/ │ ├── <세션ID>.jsonl │ └── <세션ID>.jsonl └── -Users-me-projectB/ └── <세션ID>.jsonl 프로젝트 폴더의 절대 경로에서 / 를 - 로 바꾼 이름이 하위 폴더명이 됩니다. 세션 하나가 .jsonl 파일 하나입니다. 대화와 도구 호출 기록이 한 줄씩 JSON 형태로 쌓입니다. C:\Users\사용자명\.claude\projects\ 아래에 같은 방식으로 저장됩니다. 이 구조 덕분에 프로젝트A와 B의 대화는 서로 섞이지 않습니다. B에서 작업했다고 해서 A의 기록이 지워지지도 않습니다. 3. 이전 대화를 이어가는 방법 방법 설명 claude -c ( --continue ) 현재 폴더의 가장 최근 대화를 바로 이어감 claude -r ( --resume ) 현재 폴더의 과거 세션 목록에서 골라서 이어감 /resume 이미 claude 를 실행한 상태에서 과거 세션을 불러옴 세션 조회는 현재 폴더 경로 기준 으로 이루어집니다. 그래서 프로젝트 폴더를 다른 경로로 옮기거나 이름을 바꾸면 이전 세션이 목록에 나타나지 않을 수 있습니다. 4. 새 세션에서도 유지되는 것과 사라지는 것 유지되는 것 CLAUDE.md : 세션을 시작할 때마다 자동으로 읽힙니다. 프로젝트 규칙, 코딩 컨벤션, 자주 쓰는 명령어를 여기에 적어두면 매번 다시 설명할 필요가 없습니다. 디스크의 파일 변경 사항 : 이미 저장된 코드 수정 결과는 그대로입니다. Claude가 파일을 직접 읽거나 git log , git diff 로 이전 작업 내역을 확인할 수 있습니다. 사라지는 것 이전 대화의 내용 자체, 즉 context입니다. "아까 하던 거 계속해줘"라고 해도 Claude는 맥락을 알 수 없습니다. 5. Ctrl+C 동작 작업 중 Ctrl+C를 한 번 누르면 진행 중인 작업이 중단되고, 연속으로 두 번 누르면 Claude Code가 종료됩니다. 6. 실무 팁 이어서 작업할 때는 claude -c 를 습관화합니다. 옵션 없이 실행하면 세션 파일이 계속 새로 쌓이므로 맥락이 끊깁니다. 중요한 결정은 파일로 남깁니다. 진행 상황, 설계 결정, 남은 할 일을 CLAUDE.md 나 NOTES.md 에 정리해 두면, 새 세션에서도 "NOTES.md 읽고 이어서 진행해줘" 한마디로 맥락을 복구할 수 있습니다. 저장 기록의 보안에 유의합니다. 대화가 평문으로 저장되므로, API 키나 개인정보 같은 민감한 내용을 다뤘다면 ~/.claude/ 폴더의 접근 권한을 관리하세요. 보관 기간을 확인합니다. 오래된 세션은 일정 기간(기본값은 약 30일로 알려져 있습니다)이 지나면 자동 정리될 수 있습니다. 장기 보관이 필요한 내용은 별도 파일로 남기는 편이 안전합니다. 마치며 핵심은 세 가지입니다. 세션은 폴더(프로젝트) 단위 로 저장된다. 옵션 없이 claude 를 실행하면 새 세션 이고, 이어가려면 -c , -r , /resume 을 쓴다. 새 세션에서도 CLAUDE.md 와 디스크의 파일 상태는 남으므로, 중요한 맥락은 문서로 남겨두는 것이 가장 확실하다. ※ 보관 기간, 저장 경로 같은 세부 사항은 버전에 따라 달라질 수 있으니 게시 전에 Claude Code 공식 문서(docs.claude.com)에서 최신 내용을 한 번 확인하시길 권합니다.
이번주에는 3주차에 작성했던 코드를 ORM을 이용해서 업드레이드 하는 실습을 진행했습니다. 1. 핵심 키워드 1-1. ORM ORM(Object-Relational Mapping)은 객체와 관계형 데이터베이스의 테이블을 연결해 주는 기술입니다. 개발자는 데이터베이스의 테이블을 Java 객체인 Entity로 표현하고, Repository를 통해 데이터를 조회하거나 저장할 수 있습니다. ORM을 사용하면 반복적인 CRUD 작업에서 SQL을 직접 작성하는 코드를 줄일 수 있습니다. 하지만 ORM을 사용하더라도 내부적으로는 SQL이 실행되기 때문에 SQL과 데이터베이스에 대한 이해가 필요합니다. 1-2. JPA와 Hibernate JPA(Java Persistence API)는 Java에서 ORM을 사용하기 위한 표준 명세입니다. Hibernate는 JPA의 표준을 실제로 구현한 대표적인 ORM 구현체입니다. 즉, JPA가 ORM을 사용하기 위한 규칙이라면 Hibernate는 그 규칙을 실제 코드로 동작하게 만들어 주는 구현체입니다. 1-3. Entity Entity는 데이터베이스의 테이블을 Java 클래스로 표현한 객체입니다. @Entity를 사용하면 해당 클래스가 JPA에서 관리하는 Entity임을 나타냅니다. @Id는 테이블의 기본 키를 나타내고, @Column을 사용하면 Java 필드와 데이터베이스의 컬럼을 연결할 수 있습니다. 예를 들어 book 테이블은 Book Entity로, category 테이블은 Category Entity로 표현할 수 있습니다. 1-4. Entity 관계 매핑 Entity 사이의 데이터베이스 관계를 Java 객체의 관계로 표현하는 것을 관계 매핑이라고 합니다. Book과 Category처럼 여러 개의 Book이 하나의 Category에 속하는 경우 @ManyToOne을 사용하여 다대일 관계를 표현할 수 있습니다. @JoinColumn은 관계를 연결하는 데이터베이스의 외래 키 컬럼을 지정합니다. 1-5. Persistence Context Persistence Context(영속성 컨텍스트)는 JPA가 Entity를 관리하는 공간입니다. Entity를 저장하거나 조회하면 JPA가 해당 Entity를 Persistence Context에서 관리하고, 변경 사항을 추적할 수 있습니다. 트랜잭션이 커밋되기 전까지 SQL 실행이 지연될 수 있으며, flush 시점에 변경 내용이 데이터베이스에 반영됩니다. 1-6. Repository Repository는 데이터베이스에 접근하는 역할을 담당하는 객체입니다. Spring Data JPA에서는 JpaRepository를 상속하면 기본적인 조회, 저장, 수정, 삭제 기능을 사용할 수 있습니다. 또한 메서드 이름을 이용하여 원하는 조건의 조회 기능을 만들 수 있습니다. 예를 들어 findAllByOrderByBookIdDesc()는 모든 Book을 bookId 기준 내림차순으로 조회합니다. 1-7. DTO DTO(Data Transfer Object)는 API를 통해 데이터를 주고받기 위한 객체입니다. Entity를 API에 그대로 사용하지 않고 DTO를 사용하면 데이터베이스 구조와 API의 데이터 형식을 분리할 수 있습니다. 요청 데이터를 받는 Request DTO와 응답 데이터를 전달하는 Response DTO를 별도로 만들 수 있습니다. 1-8. Validation Validation은 API로 들어오는 요청 데이터가 올바른 형식인지 확인하는 기능입니다. @Valid를 사용하면 DTO에 설정한 검증 조건을 요청 데이터에 적용할 수 있습니다. @NotNull은 값이 null인지 확인하고, @NotBlank는 문자열이 비어 있거나 공백인지 확인하며, @Size는 문자열 등의 크기를 제한할 때 사용합니다. 1-9. N+1 Query N+1 문제는 연관된 Entity를 조회하는 과정에서 예상보다 많은 SQL 쿼리가 실행되는 문제입니다. 예를 들어 게시글 10개를 한 번 조회한 후 각 게시글의 댓글을 조회하면서 10번의 추가 쿼리가 발생하면 총 11번의 쿼리가 실행됩니다. JOIN FETCH나 @BatchSize 등을 사용하여 불필요한 추가 쿼리를 줄일 수 있습니다. 1-10. Migration Migration은 데이터베이스의 테이블이나 컬럼 등의 구조 변경을 버전별로 관리하는 방법입니다. Entity의 변경 내용을 운영 데이터베이스에 자동으로 반영하는 방식은 예상하지 못한 스키마 변경이나 데이터 손실이 발생할 수 있기 때문에 주의해야 합니다. 운영 환경에서는 Flyway나 Liquibase와 같은 Migration 도구를 사용하여 스키마 변경을 명시적으로 관리할 수 있습니다. 2. 실습 2-1. 테이블을 Entity로 변환 기존 데이터베이스의 book과 category 테이블을 JPA에서 사용할 수 있도록 Entity 클래스로 변환했습니다. @Entity @Table(name = "category") @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Category { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "category_id") private Long categoryId; @Column(nullable = false) private String name; } @Entity를 사용하여 Category 클래스를 JPA가 관리하는 Entity로 지정했습니다. @Table을 통해 실제 데이터베이스의 category 테이블과 연결하고, @Id와 @GeneratedValue를 사용하여 기본 키를 매핑했습니다. 다음으로 book 테이블을 Book Entity로 만들었습니다. @Entity @Table(name = "book") @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Book { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "book_id") private Long bookId; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "category_id", nullable = false) private Category category; @Column(nullable = false, length = 100) private String title; @Column(columnDefinition = "TEXT") private String description; @Column(name = "is_available", nullable = false) private Boolean isAvailable = true; } 기존 book 테이블의 category_id가 category 테이블을 참조하고 있기 때문에 @ManyToOne과 @JoinColumn을 사용했습니다. 이를 통해 여러 개의 Book이 하나의 Category에 속하는 다대일 관계를 Entity에서도 표현했습니다. 2-2. ORM 연결과 Repository 등록 JPA를 사용하기 위해 build.gradle에 Spring Data JPA 의존성을 추가했습니다. implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 기존 3주차에서는 JdbcTemplate을 사용하여 직접 SQL을 작성했지만, 이번에는 Spring Data JPA의 JpaRepository를 사용했습니다. public interface BookRepository extends JpaRepository<Book, Long> { List<Book> findAllByOrderByBookIdDesc(); } JpaRepository<Book, Long>을 상속하면 Book Entity에 대한 기본적인 조회, 저장, 수정, 삭제 기능을 사용할 수 있습니다. 또한 findAllByOrderByBookIdDesc()와 같이 메서드 이름을 작성하면 별도의 SQL을 직접 작성하지 않아도 bookId를 기준으로 내림차순 조회할 수 있습니다. 2-3. DTO로 API의 약속 만들기 Entity를 API의 요청과 응답에 그대로 사용하지 않고 DTO를 만들었습니다. 먼저 도서를 등록할 때 사용할 요청 DTO를 작성했습니다. public record CreateBookRequest( @NotNull Long categoryId, @NotBlank @Size(max = 100) String title, String description ) { } categoryId는 반드시 입력되어야 하고, title은 비어 있으면 안 되도록 검증 조건을 추가했습니다. 다음으로 GET 요청에서 사용할 응답 DTO를 작성했습니다. public record BookResponse( Long bookId, String title, String description, String categoryName, Boolean isAvailable ) { } API에서 필요한 값만 응답하도록 BookResponse를 만들었기 때문에 Entity의 전체 구조와 API 응답 형식을 분리할 수 있었습니다. 2-4. GET /books 만들기 Entity, Repository, DTO를 만든 후 실제 GET API를 구현했습니다. 먼저 Service에서 Repository를 통해 도서를 조회하고 BookResponse로 변환했습니다. @Transactional(readOnly = true) public List<BookResponse> getBooks() { return bookRepository.findAllByOrderByBookIdDesc() .stream() .map(BookResponse::from) .toList(); } findAllByOrderByBookIdDesc()를 통해 도서를 최신순으로 조회하고, 조회된 Book Entity를 BookResponse로 변환했습니다. 그다음 Controller에서 GET 요청을 받을 수 있도록 작성했습니다. @GetMapping public List<BookResponse> getBooks() { return bookService.getBooks(); } 이렇게 하면 다음과 같은 흐름으로 요청이 처리됩니다. GET /books ↓ BookController ↓ BookService ↓ BookRepository ↓ Database ↓ BookResponse 마지막으로 Postman에서 GET http://localhost:8080/books 를 요청하여 도서 ID, 제목, 설명, 카테고리 이름, 대여 가능 여부가 최신순으로 반환되는 것을 확인했습니다. 마무리 이번 주차 워크북에서는 3주차에 작성했던 기존 코드를 발전시키는 과정이라 과제 자체는 크게 어렵지 않았지만, DTO와 Entity 같은 새로운 개념과 파일들이 추가되면서 각각의 파일이 어떤 역할을 하고 서로 어떻게 연결되는지 조금 헷갈렸습니다. 특히 DB 테이블을 Entity로 변환할 때 어노테이션을 사용해 컬럼 이름, null 가능 여부, 기본키, 관계 등을 하나씩 설정해야 해서 확인해야 할 부분이 많아 어려움을 조금 느꼈습니다. 단순히 테이블을 코드로 옮기는 것뿐만 아니라 데이터베이스와 객체의 관계까지 생각해야 한다는 점에서 ORM의 특징을 조금 더 이해할 수 있었습니다. 반면 Repository를 사용하면서 편리함을 크게 느꼈습니다. 특히 findAllByOrderByBookIdDesc()처럼 메서드 이름만으로 원하는 정렬 조건의 데이터를 조회할 수 있다는 점이 인상적이었습니다. 직접 SQL을 작성하지 않아도 필요한 데이터를 조회할 수 있다는 점에서 ORM을 사용하는 이유를 체감할 수 있었습니다. 아직 Entity, DTO, Repository, Controller 등의 관계가 완전히 익숙하지는 않지만, 각각의 역할을 구분하면서 코드를 작성해보니 앞으로 API 구조를 이해하는 데 도움이 될 것 같습니다.
이번 4주차에서는 Flutter에서 비동기 작업을 처리하는 방법과 비동기 작업의 결과에 따라 화면을 다르게 보여주는 방법을 학습했다. 특히 Future , async/await , FutureBuilder 를 이용하여 영화 데이터를 불러오는 과정을 구현하고, Loading / Empty / Error / Success 상태를 각각 처리했다. 또한 mounted 를 이용해 비동기 작업이 끝난 후 Widget의 상태를 안전하게 변경하는 방법과 shared_preferences 를 이용해 마지막으로 선택한 장르를 저장하고 앱을 다시 실행했을 때 복원하는 기능까지 구현했다. 1. Future Flutter에서 서버 요청이나 파일 읽기처럼 시간이 걸릴 수 있는 작업은 바로 결과를 반환하지 않을 수 있다. 이처럼 나중에 결과가 완료되는 작업을 표현할 때 Future 를 사용한다. Future<T> 에서 T 는 비동기 작업이 완료되었을 때 반환되는 값의 타입을 의미한다. 예를 들어 문자열을 나중에 반환하는 함수라면 다음과 같이 작성할 수 있다. Future<String> getMessage() async { return 'Hello Flutter'; } 여기서 Future<String> 은 지금 당장 String을 반환하는 것이 아니라, 나중에 String이 완료될 예정이라는 의미이다. 숫자를 반환한다면 Future<int> , 영화 목록을 반환한다면 Future<List<Movie>> 처럼 사용할 수 있다. Future<int> getNumber() async { return 10; } Future<List<Movie>> getMovies() async { return movies; } 2. async와 await async 와 await 는 비동기 작업을 처리할 때 함께 사용한다. 2-1. async 함수 뒤에 async 를 붙이면 해당 함수가 비동기 함수가 된다. Future<String> getMessage() async { return 'Hello'; } async 가 붙은 함수는 기본적으로 Future 를 반환한다. 2-2. await await 는 비동기 작업이 완료될 때까지 결과를 기다릴 때 사용한다. Future<String> getMessage() async { final message = await getMessageFromServer(); return message; } await 를 사용하면 비동기 작업이 완료된 이후의 결과를 받아서 다음 코드를 실행할 수 있다. 중요한 점은 await 를 사용하는 함수에는 async 가 필요하다는 것이다. Future<void> loadData() async { final data = await getData(); } 3. try-catch-finally try { // 예외가 발생할 수 있는 코드 } catch (e) { // 예외가 발생했을 때 실행 } finally { // 성공 여부와 관계없이 실행 } 각각의 역할은 다음과 같다. try → 실행할 코드를 작성 catch → 예외가 발생했을 때 처리 finally → 예외 발생 여부와 관계없이 마지막에 실행 예를 들어 데이터를 가져오는 과정에서 오류가 발생할 수 있다면 다음과 같이 작성할 수 있다. try { final data = await getData(); } catch (e) { print('데이터를 가져오는 중 오류가 발생했습니다.'); } finally { print('작업이 종료되었습니다.'); } 4. FutureBuilder FutureBuilder<T> 는 Future 의 결과에 따라 화면을 구성할 때 사용하는 Widget이다. 비동기 작업은 시간이 걸리기 때문에 작업이 진행되는 동안에는 Loading 화면을 보여주고, 작업이 완료되면 결과에 따라 화면을 보여줘야 한다. 이때 FutureBuilder 를 사용할 수 있다. FutureBuilder<String>( future: getMessage(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const CircularProgressIndicator(); } return Text(snapshot.data ?? ''); }, ) 여기서 FutureBuilder<String> 의 String 은 Future가 최종적으로 반환하는 데이터의 타입이다. 즉, Future<String> 을 사용한다면 FutureBuilder<String> 처럼 작성할 수 있다. 4-1. AsyncSnapshot FutureBuilder 의 builder 에서는 snapshot 을 통해 비동기 작업의 상태와 결과를 확인할 수 있다. 대표적으로 다음과 같은 값을 확인할 수 있다. snapshot.connectionState snapshot.data snapshot.hasError 예를 들어 Loading 상태인지 확인하려면 다음과 같이 작성할 수 있다. if (snapshot.connectionState == ConnectionState.waiting) { return const CircularProgressIndicator(); } 오류가 발생했는지는 다음과 같이 확인할 수 있다. if (snapshot.hasError) { return const Text('오류가 발생했습니다.'); } 그리고 정상적으로 데이터가 전달되었다면 snapshot.data 를 이용해 결과를 가져올 수 있다. 5. mounted Flutter에서는 비동기 작업이 진행되는 동안 Widget이 화면에서 사라질 수 있다. 예를 들어 화면에서 데이터를 불러오는 작업을 시작했는데 사용자가 그 전에 다른 화면으로 이동할 수도 있다. 이 상태에서 비동기 작업이 끝난 후 setState() 를 호출하면 문제가 발생할 수 있다. 따라서 비동기 작업이 끝난 후 현재 Widget이 아직 화면에 존재하는지 확인할 수 있다. 이때 사용하는 것이 mounted 이다. Future<void> loadData() async { final data = await getData(); if (!mounted) { return; } setState(() { // 상태 변경 }); } mounted 가 false 라면 해당 State가 더 이상 Widget tree에 연결되어 있지 않다는 의미이다. 따라서 mounted 를 확인한 후 setState() 를 호출하면 안전하게 상태를 변경할 수 있다. 6. shared_preferences 앱을 종료했다가 다시 실행하면 일반적인 변수에 저장한 값은 사라진다. 예를 들어 다음과 같은 변수에 장르를 저장했다면 String selectedGenre = '드라마'; 앱을 종료하고 다시 실행했을 때 다시 초기값으로 돌아간다. 앱을 다시 실행해도 유지해야 하는 간단한 설정값은 로컬 저장소에 저장할 수 있다. Flutter에서는 shared_preferences 패키지를 사용할 수 있다. flutter pub add shared_preferences shared_preferences 는 문자열, 숫자, boolean과 같은 간단한 데이터를 로컬에 저장할 때 사용할 수 있다. 이번 실습에서는 새로운 코드에서 비동기 방식으로 동작하는 SharedPreferencesAsync 를 사용했다. final preferences = SharedPreferencesAsync(); 값을 저장할 때는 다음과 같이 사용할 수 있다. await preferences.setString( 'selected_genre', '드라마', ); 저장된 값을 가져올 때는 다음과 같이 작성한다. final genre = await preferences.getString( 'selected_genre', ); 여기서 selected_genre는 저장할 데이터를 구분하기 위한 Key이다. 마무리 이번 4주차에서는 Flutter에서 비동기 작업을 처리하는 방법과 그 결과에 따라 화면의 상태를 나누는 방법을 학습했다. 특히 Future<String> 과 같은 Future<T> 의 개념부터 async/await , FutureBuilder , AsyncSnapshot 까지 비동기 작업이 어떤 흐름으로 처리되는지 이해할 수 있었다. 실습에서는 FakeMovieService 를 이용해 실제 API 없이도 영화 데이터를 비동기로 불러오는 상황을 만들었고, FutureBuilder 를 이용하여 Loading, Empty, Error, Success 상태를 각각 구현했다. 또한 Error 상태에서는 다시 시도 버튼을 눌렀을 때만 새로운 Future를 생성하도록 하면서 비동기 작업을 다시 실행하는 방법도 익혔다. mounted 를 이용해서 비동기 작업이 끝난 후 Widget이 아직 존재하는지 확인하는 방법도 배웠고, SharedPreferencesAsync 를 이용해서 사용자가 마지막으로 선택한 장르를 저장하고 앱을 다시 실행했을 때 복원하는 기능도 구현했다. 3주차에서는 화면을 연결하고 데이터를 전달하는 과정에 집중했다면, 4주차에서는 데이터를 불러오는 과정에서 발생할 수 있는 여러 상태를 처리하는 방법까지 확장해서 학습할 수 있었다. 처음에는 Future와 FutureBuilder가 낯설었지만 실제로 Loading부터 Error, Empty, Success까지 직접 구현해보면서 비동기 데이터가 화면에 표시되기까지의 흐름을 조금씩 이해할 수 있었다.
클린코드 클린 코드는... 코드 스멜이 없고 가독성 이 높아 읽고 이해하기 쉬운 코드 단순해서 변경 및 확장 이 용이한 코드 의도 가 명확히 드러나 동료가 신뢰할 수 있는 코드 유연하고(Flexible), 견고하며(Robust), 유지보수하기 쉬움(Maintainable) ⇒ 개발자의 논리적인 사고 흐름이 반영된 좋은 코드는 협업을 원활하게 하고 시간이 지나도 견고하게 유지되며 빠른 의사소통으로 팀 전체의 생산성을 높인다 클린코드 팁 : 모듈화는 기본, 코드는 한 번 작성하고 여러 번 본다 ⇒ 처음부터 잘 쓰자 ⬌ 코드 스멜 잠재적인 문제를 나타내는 징후 경직되고(Rigid), 깨지기 쉽고(Fragile), 재사용하기 어려움(Immobile) 리팩토링 클린 코드를 만들어가는 핵심적인 활동이자 코드 구조를 건강하게 개선하는 활동 소프트웨어의 겉보기 동작은 그대로 유지한 채, 내부 구조를 변경하여 이해하고 수정하기 쉽게 만드는 과정 (버그를 잡거나 새로운 기능을 추가하는 것 X) 객체지향설계 원칙과 실천 요소의 관계 좋은 설계(Object-Oriented Analysis and Design)은 좋은 구현(Object-Oriented Programming)을 이끈다! SOLID 원칙으로 설계의 뼈대와 방향을 잡고 → 디자인 패턴으로 문제를 풀며 → 리팩토링으로 다듬어 품질을 높이고 → 결국 클린코드로 완성해간다 ** 클린코드는 결과물이자 개발자가 추구해야 할 목표이며 방향!** 객체지향설계의 5가지 핵심 원칙 : SOLID S O L I D Single Responsibility Open /Closed Liskov Substitution Interface Segregation Dependency Inversion 단일 책임 원칙 개방-폐쇄 원칙 리스코프 치환 원칙 인터페이스 분리 원칙 의존성 역전 원칙 SOLID는 변화에 강하고 재사용에 유리한 클래스 구조를 만드는 원칙이다 소프트웨어는 끊임없이 변화하고 성장하는데, 좋은 설계를 통해 미래의 변경에 유연하게 대처할 수 있게 해야 한다 SOLID를 적용하면 유지보수하기도 쉽고, 확장도 가능하고, 재사용성을 높이며, 테스트하기에도 편하다 SOLID는 서로 개념적으로 연관되어 있으며, 모든 원칙을 적용할 필요는 없다 결합도는 낮게! 응집도는 높게! 결합도 : 모듈과 모듈 간의 의존 정도 결합도가 낮으면 → 클래스 간의 관계가 느슨해서 자유로운 교체 가능 응집도 : 한 모듈 내 구성 요소의 연관 정도 응집도가 높으면 → 클래스 내부가 하나의 목적에 집중 SOLID, 왜 필요한가... 기존 코드를 어떻게 개선해야 할까에 대한 방향을 제시하기 때문에, SOLID 원칙을 따르면 코드의 변경 지점이 명확해지고 의존성이 낮아져 부작용 없이 안전하게 코드를 개선할 수 있다 처음부터 유지보수와 확장이 용이한 구조를 만들고, 리팩토링 시 얽혀있던 의존성을 끊고 코드의 구조를 개선하여 미래에 발생할 여러 문제점들을 예방하고 안전하게 해결할 수 있다 SOLID, 어디에 활용되는가... 리팩토링뿐만 아니라 스프링 프레임워크의 핵심 철학이다 스프링의 핵심인 제어의 역전(IoC)와 의존성 주입(DI)은 의존성 역전 원칙(DIP)과 개방-폐쇠 원칙(OCP)을 가장 잘 구현한 대표적인 예이다 또한, MSA(마이크로서비스아키텍처)와 같이 변화에 유연하게 대응해야 하는 현대적인 아키텍처를 설계할 때 기본적으로 각 서비스의 역할을 명확히 나누고(SRP, ISP) t서비스 간의 결합도를 낮추는(DIP) 원칙이 활용된다 SRP(Single Responsibility) 단일 책임 원칙 클래스는 단 하나의 기능(책임)에 집중하도록 분리한다 ⇒ 응집도 ↑ 여러 책임을 가지면 한 책의 변경이 다른 책임에 영향을 주기 때문 코드를 이해하기 쉽게 만들고 코드를 변경했을 때의 영향이 명확해진다 ⇒ 재사용성 ↑ //before class ReportService { public void generateReport() { /* 보고서 생성 */ } public void sendemail() { /* 이메일 전송 */ } } → ReportService가 보고서 생성과 이메일 전송을 모두 처리함 //after class ReportGenerator { public void generateReport() { /* 보고서 생성 */ } } class EmailSender { public void sendemail() { /* 이메일 전송 */ } } → 보고서 생성과 이메일 전송 기능이 분리됨, ReportGenerator 클래스는 보고서 생성이라는 기능만, EmailSender 클래스는 이메일 전송이라는 기능만 수행하고 있음 OCP(Open/Closed) 개방-폐쇄 원칙 확장에는 열려있고, 수정에는 닫혀 있어야 한다 (수정은 기존 코드 뜯어 고치는 것, 확장은 기존 코드에 새 기능을 연결시키는 것) 기존 코드를 변경하지 않고도 새로운 기능을 추가할 수 있어야 함 ⇒ 유연성 ↑ 추상화(인터페이스, 추상 클래스)에 의존하고, 구체적인 구현 클래스에 의존하지 않는다 //before public class PaymentProcessor { public void process(String paymentType) { if ("creditCard".equals(paymentType)) { // 신용카드 결제 로직 } else if ("kakaoPay".equals(paymentType)) { // 카카오페이 결제 로직 } // 새로운 결제 수단(ex: NaverPay)이 추가될 때마다 이 부분을 수정해야 함! } } → 새로운 결제 수단 추가 시 기존 코드를 수정해야 함 //after // 결제 수단 인터페이스 public interface PaymentMethod { void pay(); } // 신용카드 결제 public class CreditCard implements PaymentMethod { @Override public void pay() { /* 신용카드 결제 로직 */ } } // 카카오페이 결제 public class KakaoPay implements PaymentMethod { @Override public void pay() { /* 카카오페이 결제 로직 */ } } // 결제 처리기 public class PaymentProcessor { public void process(PaymentMethod paymentMethod) { paymentMethod.pay(); // 어떤 결제 수단이 와도 코드는 동일 } } → 새로운 결제수단 추가 시 기존 코드를 수정하지 않고 새로운 클래스를 추가하고 인터페이스를 구현하여 확장 가능 LSP(Liscov Substitution) 리스코프 치환 원칙 ** 하위 타입은 언제나 상위 타입으로 대체될 수 있어야 한다 ** 자식 클래스는 부모 클래스가 사용되는 곳에 문제없이 들어갈 수 있어야 한다 ⇒ 자식 클래스는 부모 클래스의 행동 규약을 위반해서는 안 됨 ⇒ 상속은 'IS-A' 관계를 명확히 따를 때 사용 //before class Bird { public void fly() { /* 날기 */ } } class Ostrich extends Bird { @Override public void fly() { throw new UnsupportedOperationException("타조는 못 날아요"); } } → Bird 클래스를 상속한 Ostrich 클래스도 fly()를 오버라이드해야 하므로 문제(예외)가 발생함 //after interface Bird { void move(); } class FlyingBird implements Bird { public void move() { System.out.println("날아요"); } } class WalkingBird implements Bird { public void move() { System.out.println("걸어요"); } } → Bird 인터페이스를 만들어 FlyingBird, WalkingBird로 분리함 // 이게 교수님이 이전에 말씀하셨던 상속 회피와 관련되는 내용인 것 같다 ISP(Interface Segregation) 인터페이스 분리 원칙 ** 클라이언트는 자신이 사용하지 않는 메소드에 의존해서는 안된다 ** 하나의 거대한 인터페이스보다, 여러 개의 구체적인 인터페이스가 낫다 ⇒ 결합도 ↓ 클래스가 불필요한 메소드를 구현하지 않도록 분리한다 ⇒ SRP와 관련됨 인터페이스 변경 시에 영향 받는 클래스를 최소화한다 //before interface Worker { void work(); void eat(); } class Robot implements Worker { public void work() { /* 작업 */ } public void eat() { throw new UnsupportedOperationException(); } } → Worker 인터페이스에 work(), eat()이 있기 때문에 Robot이 불필요한 eat()까지 구현해야 함 //after interface Workable { void work(); } interface Eatable { void eat(); } class Robot implements Workable { public void work() { /* 작업 */ } } → 기능을 기준으로 Workable, Eatable 인터페이스로 분리하여 Robot이 필요한 기능만 구현할 수 있도록 함 DIP(Dependency Inversion) 의존성 역전 원칙 ** 상위 모듈은 하위 모듈에 의존해서는 안 되고, 둘 다 추상화에 의존해야 한다 ** 의존 관계를 맺으려면 변화 빈도가 높은 구체적인 구현 클래스(하위 모듈)에 직접 의존하는 것이 아니라, 변화 빈도가 낮은 인터페이스나 추상 클래스(추상화)에 의존하라는 의미 의존성의 방향을 역전시켜 유연한 관계를 만든다 ⇒ OCP를 가능하게 하는 핵심 원칙 //before class Light { public void turnOn() { System.out.println("💡 불 켜짐"); } public void turnOff() { System.out.println("💡 불 꺼짐"); } } class SmartHomeSwitch { private Light light; public SmartHomeSwitch() { this.light = new Light(); // 구체 클래스에 직접 의존 } public void operate(String command) { if (command.equals("on")) light.turnOn(); else light.turnOff(); } } → SmartHomeSwitch Light라는 구현 클래스에 직접 의존하고 있어서 다른 장치는 제어할 수가 없음, 추가하려면 코드를 수정해야 함 //after // 추상화 interface Device { void turnOn(); void turnOff(); } // 다양한 가전제품 구현 class Light implements Device { public void turnOn() { System.out.println("💡 불 켜짐"); } public void turnOff() { System.out.println("💡 불 꺼짐"); } } class AirConditioner implements Device { public void turnOn() { System.out.println("❄️ 에어컨 ON"); } public void turnOff() { System.out.println("❄️ 에어컨 OFF"); } } class CoffeeMachine implements Device { public void turnOn() { System.out.println("☕ 커피 추출 시작"); } public void turnOff() { System.out.println("☕ 커피 추출 종료"); } } // 스마트홈 스위치는 추상화에만 의존하여 여러개의 device와 연결 public class SmartHomeSwitch { private final List<Device> devices; public SmartHomeSwitch(List<Device> devices) { this.devices = devices; } public void operateAll(String command) { for (Device device : devices) { if ("on".equals(command)) device.turnOn(); else device.turnOff(); } } public void operateByType(Class<? extends Device> type, String command) { for (Device device : devices) { if (type.isInstance(device)) { if ("on".equals(command)) device.turnOn(); else device.turnOff(); } } } } → Device라는 인터페이스를 도입하여 사용함으로써 SmartHomeSwitch는 추상화에만 의존하여 여러 개의 장치와 연결되어 있음, 어떤 장치든 Device만 구현하면 즉시 연결할 수 있어 확장성이 높음 제어의 역전(IoC)과 의존성 주입(DI) ** 제어의 역전(IoC, Inverison of Control) ** 전통적인 방식에서는 개발자가 작성한 코드가 객체를 생성하고 의존성을 연결하는 등 모든 제어의 흐름을 직접 관리함 IoC는 이 제어의 흐름을 역전시키는 것으로, 객체의 생성부터 생명주기 관리까지 모든 것을 개발자가 아닌 프레임워크 에 위임함 개발자는 어떤 부품이 필요한지만 알려주고, 조립은 프레임워크라는 전문가가 알아서 해주는 원리! ** 의존성 주입(DI, Dependency Injection) ** DI는 IoC를 구현하는 대표적인 기술 클래스 내부에서 new 를 사용해 의존 객체를 직접 생성하는 것이 아니라, 외부(프레임워크)에서 의존 객체를 전달(주입)받는 방식 생성자 주입 : 스프링에서 가장 권장하는 방식 의존성 불변(안전성) : final 키워드를 사용할 수 있어 의존성이 런타임에 변경되는 것 방지 필수 의존성 명확 : 객체 생성 시점에 모든 필수 의존성이 주입되어야 하므로, 의존성이 누락되는 경우가 없음 테스트 용이 : DI 프레임워크 없이 순수 자바 코드로 객체를 쉽게 생성하고 Mock 객체 주입 가능 Setter 주입 : 의존성이 필수가 아닌 선택사항일 때 유용, Setter 가 호출되기 전까지 의존성은 null 필드 주입 : 코드가 가장 간결하고 편리하지만, 테스트가 어렵고 의존성이 숨겨짐 ⇒ 권장 X 간략한 소감 이번 수업이 정말 자바에 대한 새로운 시야를 열어준 느낌이다. SOLID 원칙을 전부 보고 나니까 처음에 왜 모든 개념이 연결되어 있다고 말씀하셨는지 알 것 같다. 결국 좋은 코드를 짜려면 가독성이나 유지보수성이 중요한데, SOLID 원칙이 그걸 위해서 엄청 쉽게 하나하나 풀어서 설명해주는 느낌이다... 교수님께서 전에 수업 중 코드 설명하시면서 복잡하고 있어보이는 게 좋은 게 아니라 간단하고 확장하기 편한 게 좋은 코드라고 말씀하셨던 적이 여러 번 있었다. 그때마다 사실은 뭔가 어렴풋이...? 느낌적으로만,.. 받아들였던 것 같은데, 이런 SOLID 원칙에 의한 것이었음을 알게 되어서 비로소 이해가 되는 기분이다. 그동안 생각해왔던 무조건 짧고 간결한 코드!가 아니라 코드는 길어보일지라도 실제 기능에 따라 쪼개고 더 이해하기 쉽게 만드는 게 훨씬 중요하다는 걸... 어찌 보면 너무나 당연한 걸 이제야 제대로 실감해버렸다. 아직도 코드를 쓰는 것은 너무 어렵게 느껴지지만, (일단 돌아가는 것에 급급하고... 늘고 있는지도 모르겠지만 ᅲᅲ) 내가 추구해야 하는 방향성에 대해서 알게 되어서 정말 유익했다고 생각한다.
1. 기본 구조 Skill은 폴더 하나와 그 안의 SKILL.md 로 구성됩니다. 프로젝트용은 .claude/skills/ 에, 개인 전역용은 ~/.claude/skills/ 에 둡니다. .claude/skills/ └── code-review/ # 폴더명 = name (kebab-case) ├── SKILL.md # 필수 (대문자) ├── reference.md # 선택: 상세 문서 └── scripts/ # 선택: 보조 스크립트 .claude/skills/<skill-name>/SKILL.md 를 만들고, name 은 디렉터리명과 일치해야 합니다. 폴더명은 소문자, 숫자, 하이픈만 쓰고, 파일명 SKILL.md 는 반드시 대문자입니다. 2. Frontmatter (공식 필수 필드) 파일 맨 위에 --- 로 감싼 YAML 블록을 두며, 필수 필드는 두 개입니다. --- name: code-review description: 코드 변경사항을 리뷰한다. 사용자가 "리뷰해줘", "PR 검토", "코드 점검"을 요청할 때 사용. --- 필드 역할 name Claude Code에서 /슬래시 명령어 가 됩니다. description Claude가 이 skill을 언제 로드할지 판단하는 기준입니다. description 이 가장 중요합니다. Claude는 이 문장을 보고 skill을 자동으로 호출할지 결정하기 때문입니다. 그래서 다음을 지키는 것이 좋습니다. 무엇을 하는지 + 언제 쓰는지 를 함께 적습니다. 구체적인 트리거 문구를 포함합니다. 사용자가 실제로 쓸 법한 표현을 넣으세요. 너무 모호한 설명(예: "코드 도움")은 오작동의 원인이 됩니다. allowed-tools 같은 선택 필드도 있는 것으로 보이지만, 이번 검색에서는 공식 문서 원문을 직접 확인하지 못했습니다. 정확한 목록은 공식 문서(docs.claude.com)를 확인해 주세요. 3. 본문(Body) 작성: Goal / Instructions / Constraints Goal, Instructions, Constraints는 공식 예약 키워드가 아니라 관례적인 섹션 이름입니다. 본문은 자유로운 Markdown이며, 아래 구조가 Claude가 따르기 쉬워서 많이 쓰입니다. 섹션 내용 Goal 이 skill이 달성할 최종 결과물 When to use 적용 상황, 입력 조건 (description 보완) Instructions 순서가 있는 단계별 작업 절차 Constraints 하지 말아야 할 것, 지켜야 할 규칙, 범위 제한 Output format 결과물 형식, 템플릿 Examples 입력/출력 예시 작성 팁은 다음과 같습니다. 로딩 방식을 이해하세요. frontmatter(name, description)는 항상 로드되고, 본문은 트리거될 때만 로드됩니다. 이를 progressive disclosure라고 부릅니다. 본문을 짧게 유지하세요. SKILL.md는 500줄 미만으로 유지하고, 상세 내용(DB 스키마, API 문서 등)은 별도 참조 파일로 분리하는 것이 권장됩니다. 본문에서 상대 경로로 링크하면 됩니다. 지시는 " 하지 마라"보다 " 해라"처럼 명확하고 검증 가능하게 쓰고, 금지 사항은 Constraints에 모읍니다. 4. 자주 쓰이는 Skill 예시 예시 1: 코드 리뷰 --- name: code-review description: 변경된 코드를 리뷰하고 개선점을 제안한다. "코드 리뷰", "PR 검토", "변경사항 점검" 요청 시 사용. --- # Code Review ## Goal 변경된 코드의 버그, 보안 이슈, 가독성 문제를 찾아 우선순위별로 보고한다. ## Instructions 1. `git diff`로 변경 범위를 확인한다. 2. 변경된 파일을 읽고 주변 코드 맥락을 파악한다. 3. 다음 관점으로 점검한다: 로직 오류 → 보안 → 성능 → 가독성 → 테스트 누락 4. 결과를 아래 출력 형식으로 정리한다. ## Constraints - 요청 없이 코드를 직접 수정하지 않는다. - 변경되지 않은 파일은 지적하지 않는다. - 취향 차이 수준의 스타일 지적은 최소화한다. ## Output format - 🔴 Critical / 🟡 Suggestion / 🟢 Good 로 분류 - 각 항목: `파일:라인` – 문제 – 제안 예시 2: 커밋 메시지 작성 --- name: commit-message description: 스테이징된 변경사항으로 Conventional Commits 형식의 커밋 메시지를 작성한다. "커밋 메시지", "commit" 요청 시 사용. --- ## Goal 프로젝트 컨벤션에 맞는 커밋 메시지를 생성한다. ## Instructions 1. `git diff --staged`로 변경 내용을 확인한다. 2. `type(scope): subject` 형식으로 작성한다. (feat, fix, docs, refactor, test, chore) 3. 필요하면 본문에 "왜 변경했는지"를 적는다. ## Constraints - subject는 50자 이내, 마침표 없이 작성한다. - 직접 `git commit`을 실행하지 말고 메시지만 제안한다. 예시 3: 테스트 작성 --- name: write-tests description: 지정한 함수나 모듈의 단위 테스트를 작성한다. "테스트 작성", "테스트 추가", "커버리지 보강" 요청 시 사용. --- ## Goal 기존 테스트 스타일에 맞춰 의미 있는 테스트를 추가한다. ## Instructions 1. 프로젝트의 기존 테스트 파일에서 프레임워크와 네이밍 규칙을 파악한다. 2. 정상 케이스, 경계값, 예외 케이스 순으로 작성한다. 3. 작성 후 테스트를 실행해 통과 여부를 확인한다. ## Constraints - 프로덕션 코드는 수정하지 않는다. - 외부 네트워크 호출은 mock 처리한다. 그 밖에 자주 만드는 유형으로 PR 설명 작성, 릴리스 노트 생성, DB 마이그레이션 점검, 프로젝트 코딩 컨벤션 가이드, 문서 감사(audit) 등이 있습니다. 5. 실무 체크리스트 name 이 폴더명과 일치하는가 (kebab-case) description 에 기능 + 트리거 문구 가 모두 들어 있는가 한 skill이 한 가지 일만 하는가 (큰 작업은 분리) 본문이 500줄 이하이고, 상세 내용은 별도 파일로 뺐는가 Constraints에 금지 사항과 범위를 명시했는가
Best 17 Sites to Buy,, Old GitHub Accounts (New & Aged) Buy GitHub Accounts: What You Should Know Before Purchasing ❤️💠♻️❇️🌐👉🏽✅🔷🏓🌍🔆💟✔️If You Are Interested Just Click Here 👉 ❤️💠♻️❇️🌐👉🏽✅🔷🏓🌍🔆💟✔️Telegram:@Usavcsmm ❤️💠♻️❇️🌐👉🏽✅🔷🏓🌍🔆💟✔️WhatsApp: +1(657) 462-1328 ❤️💠♻️❇️🌐👉🏽✅🔷🏓🌍🔆💟✔️E-mail: usavcsmm@gmail.com ❤️💠♻️❇️🌐👉🏽✅🔷🏓🌍🔆💟✔️Visit now: https://usavcsmm.com/product/buy-google-voice-accounts/ GitHub has become one of the world's most widely used platforms for software development, version control, collaboration, and open-source projects. Developers, agencies, startups, and businesses rely on GitHub to manage repositories, collaborate with teams, publish projects, and build professional portfolios. Because established GitHub profiles can appear more developed than newly created accounts, some users search for services using terms such as “Buy GitHub Accounts.” However, purchasing an existing account can involve significant security, ownership, authenticity, and policy concerns. GitHub's Terms of Service place responsibility for an account and activity performed through it on the account holder, while its Acceptable Use Policies prohibit various forms of fake accounts and inauthentic activity. For businesses and developers, the better approach is to understand what they actually need from a GitHub account and choose a legitimate setup that provides those capabilities without putting valuable projects or credentials at unnecessary risk. This guide explains what buyers should consider, the features that matter, common risks, and safer alternatives for establishing a professional GitHub presence. What Does “Buy GitHub Accounts” Mean? The phrase “Buy GitHub Accounts” generally refers to the search for pre-existing GitHub accounts offered by third parties rather than creating a new account directly through GitHub. Such accounts may be advertised with characteristics such as: An existing account history A pre-established profile Previous repository activity Existing followers or social connections An older account creation date Different profile configurations Claimed verification or reputation However, these characteristics do not automatically make an account trustworthy or suitable for business use. An account's history may be difficult to verify, and the original creator may retain information or recovery mechanisms that create security problems for a subsequent user. In addition, GitHub's policies specifically address fake accounts, inauthentic activity, impersonation, and secondary markets associated with inauthentic activity. Why Do People Search for GitHub Accounts? There are several legitimate business reasons someone may search for an established GitHub presence. Faster Project Setup A developer starting a new project may want to establish a professional profile quickly. Rather than focusing on account age, the practical goal should be setting up repositories, documentation, contribution workflows, and security controls efficiently. Professional Development GitHub can function as an important part of a developer's professional portfolio. A well-organized profile can showcase: Software projects Open-source contributions Programming experience Documentation skills Collaboration history Technical interests Building these assets organically provides a more reliable representation of a developer's work. Business Collaboration Companies often need GitHub access for development teams, repositories, organizations, and project management. Instead of purchasing individual accounts, businesses should use GitHub's organizational and access-management capabilities appropriate to their needs. Project Migration Sometimes the real requirement behind a search for an established account is actually the need to transfer an existing project. GitHub provides official repository-transfer functionality that allows eligible repositories to be transferred between personal accounts and organizations. Important Risks of Buying GitHub Accounts Before considering any third-party account marketplace, it is important to understand the potential disadvantages. Account Ownership Problems An account purchased from another person may not provide genuine long-term ownership. The original creator could potentially have recovery information, connected credentials, or historical access that creates complications. GitHub's Terms state that users are responsible for keeping their accounts secure and for activity performed while signed in to or using their accounts. Suspension Risk GitHub maintains policies against fake accounts and inauthentic activity. It also prohibits certain secondary markets associated with the proliferation of inauthentic activity. Enforcement can include account suspension, termination, or content removal. ❤️💠♻️❇️🌐👉🏽✅🔷🏓🌍🔆💟✔️If You Are Interested Just Click Here 👉 ❤️💠♻️❇️🌐👉🏽✅🔷🏓🌍🔆💟✔️Telegram:@Usavcsmm ❤️💠♻️❇️🌐👉🏽✅🔷🏓🌍🔆💟✔️WhatsApp: +1(657) 462-1328 ❤️💠♻️❇️🌐👉🏽✅🔷🏓🌍🔆💟✔️E-mail: usavcsmm@gmail.com ❤️💠♻️❇️🌐👉🏽✅🔷🏓🌍🔆💟✔️Visit now: https://usavcsmm.com/product/buy-google-voice-accounts/ This means an account's apparent age or history should never be treated as a guarantee of continued access. Security Concerns Third-party accounts may have unknown histories. You may not know: Who previously controlled the account Which devices accessed it Whether recovery information remains connected Whether credentials were reused elsewhere Whether previous activity violated platform rules For developers managing private repositories, source code, API credentials, or proprietary information, these concerns are particularly important. Reputation and Authenticity Issues An established profile may contain activity that does not accurately represent the current owner. Using another person's historical profile can also create questions about authorship and identity. GitHub's impersonation policy prohibits misleading others about a person's identity or association with another individual or organization. GGitHub Docs Features to Look for in a Legitimate GitHub Setup Instead of focusing primarily on account age, evaluate the features that actually support your work. Secure Account Access Security should be the first priority. Use a unique password, appropriate authentication methods, and current recovery information. Professional Profile A strong developer profile should clearly communicate: Your name or professional identity Areas of technical expertise Relevant projects Professional website or portfolio Appropriate contact information Contributions and achievements Organized Repositories High-quality repositories should have meaningful names, clear README files, documentation, licensing information where appropriate, and sensible project structures. Appropriate Team Permissions For companies and development teams, use organizational access controls instead of sharing personal credentials. This creates a clearer separation between individual identities and business projects. Repository Transfer Options If you already have a legitimate project and need to move it, GitHub provides an official repository-transfer process. Eligible repositories can be transferred to another personal account or organization when the applicable requirements are met. Benefits of Building a GitHub Account Properly Creating and developing your own GitHub presence offers several long-term benefits. Authentic Professional Reputation Your contributions represent your actual experience. This can be more valuable to employers, clients, collaborators, and open-source communities than an artificially established profile. Better Security You control your own credentials, recovery methods, authentication settings, repositories, and account history. Greater Transparency An organically developed profile gives other developers a clearer picture of your technical interests and contributions. Long-Term Stability You avoid relying on a third-party seller or former account owner for continued access. Better Compliance Using GitHub according to its Terms of Service and Acceptable Use Policies reduces the risk associated with prohibited or inauthentic activity. GitHub explicitly says that enforcement actions can include restricting or terminating access to an account. How to Build a Professional GitHub Presence If your objective is to establish a strong GitHub profile quickly, follow a structured process. Step 1: Create Your Own Account Start with an account you control and configure it using your real professional identity or an appropriate business identity. Step 2: Secure the Account Set up strong authentication and make sure recovery information is accurate and under your control. Step 3: Build Quality Repositories Publish useful projects rather than attempting to create artificial activity. Include clear documentation and explain what each project does. Step 4: Improve Your Profile Add a concise biography, relevant technologies, portfolio information, and links to appropriate professional resources. Step 5: Contribute Consistently Participate in projects where you can make meaningful contributions. Quality contributions are generally more valuable than artificially increasing activity. Step 6: Use Organizations for Teams Businesses should consider GitHub organizations and appropriate permissions when multiple people need access to company repositories. Is Buying GitHub Accounts Worth It? The answer depends on what someone actually needs. If the goal is simply to obtain an account quickly, purchasing a third-party account may appear convenient. However, convenience does not remove the potential security, ownership, authenticity, or policy issues associated with transferred credentials and established account histories. For many users, the underlying requirement is not an old account at all. It may be a professional developer profile, repository management, team access, project migration, or a stronger online presence.
26년 1학기 산업공학종합설계에서 진행한 Cross-corpus 환경에서의 병리 음성 판별 연구의 정리이다. 데이터 문제와 AIHUB 안심존 이슈 등 여러 장애물 때문에 성공적인 연구가 되진 못했지만, 이후 연구 주제로 발전시켜 진행할 예정이다. 1. 문제 정의 음성으로 정상·병리 여부를 구분하는 모델을 만들었다고 해보자. 익숙한 데이터의 테스트셋에서 좋은 점수를 얻었다면, 다른 병원이나 다른 나라에서 수집한 음성에도 그대로 적용할 수 있을까? 실제 임상 현장처럼 학습에 사용되지 않은 새로운 데이터 환경에 적용될 경우에는 데이터셋 간의 분포 불일치(domain shift) 로 인해 성능이 크게 저하되는 문제가 발생한다. 병리 음성 데이터는 녹음 장비의 주파수 특성이나 주변 소음과 같은 물리적 요인뿐만 아니라 화자의 언어적 배경, 발성 방식, 피실험자 집단의 인구통계학적 특성 등 다양한 환경적 변수에 민감하게 영향을 받기 때문이다. 본 연구는 이러한 문제의식에서 출발하여, 세 개의 병리 음성 코퍼스를 대상으로 교차 코퍼스 환경에서 발생하는 일반화 실패의 구조를 진단하고, 나아가 제한된 타겟 데이터만으로 이를 회복할 수 있는 전이학습 전략의 효과를 검증한다. 2. 데이터 설명 본 연구에서는 네 개의 병리 음성 데이터베이스를 사용하였다: Saarbrücken Voice Database(SVD), Advanced Voice Function Assessment Databases(AVFAD), Far Eastern Memorial Hospital database(FEMH), 그리고VOice ICar fEDerico II database(VOICED).(Pützer & Barry, 2008)(Jesus et al., 2017)(Wang, 2026)(Verde & Sannino, 2018) 코퍼스 간 언어적 변수의 영향을 최소화하기 위해 모든 실험은/a/지속발성 녹음만을 대상으로 수행하였으며, 이를 통해 언어적 정보에 따른 변이를 줄이고 데이터셋 간 비교 가능성을 확보하였다. 3. 활용 모델 본 연구에서는 데이터 규모(DB당 약2,000건)와 계산 효율을 고려하여ResNet18을 채택하였다. ResNet50 이상의 모델은 파라미터 수가25M을 초과하여 소규모 데이터에서 과적합 위험이 높은 반면, ResNet18은 약11M의 파라미터로 충분한 표현력을 확보하면서도 학습 안정성을 유지할 수 있다.(Geng et al., 2025) 본 연구에서는 그림 2와 같이 ImageNet으로 사전학습된 ResNet18을 기반 모델로 사용한다. 입력은Mel-spectrogram을 224×224×3 크기로 변환한 이미지이며, conv1과 bn1을 거쳐 네 개의 잔차 블록(layer1 layer4)을 순차적으로 통과한다. 이후Global Average Pooling을 통해 512차원 임베딩 벡터로 압축되며, Dropout(p=0.5)과 이진 분류를 위한 완전연결층을 거쳐 정상 또는 병리로 분류된다. 512차원 임베딩은 분류 외에도 코퍼스 간 표현 분포 분석에 활용된다. 그림에서 점선으로 표시된 초기 레이어(conv1, bn1, layer1)는 사전학습된 저수준 특징을 보존하기 위해 고정하고, 나머지 레이어(layer2 layer4)는 학습 가능하도록 설정한다. 본 연구에서는 레이어별로 학습률을 달리 적용하는 판별적 학습률(discriminative learning rate) 전략을 사용한다. (Ro & Choi., 2020) 일반적인 전이학습에서 단일 학습률을 전체 레이어에 동일하게 적용할 경우, 사전학습으로 획득한 표현이 손상될 위험이 있다. 이를 방지하기 위해 backbone에는 낮은 학습률(1e-5)을 적용하여 사전학습된 특징 표현을 유지하고, 새로운 분류 과제에 직접 관여하는 분류층에는 높은 학습률(3e-4)을 적용하여 빠른 적응을 유도한다. 4. 전처리 및 학습 각 데이터베이스는 샘플링 레이트와 녹음 길이가 상이하므로 모델 입력의 일관성을 확보하기 위한 전처리가 필요하다. 전처리된 신호는 병리 음성 분류에서 CNN 기반 모델의 입력으로 자주 사용되는 Mel-spectrogram으로 변환하며(Javanmardi et al., 2023; Farazi et al., 2024), 변환에 사용된 주요 파라미터는 <표 3>와 같다. 이후 최솟값–최댓값 정규화를 적용한 후 단일 채널을 3채널로 복제하여 224×224 크기로 조정한다. 본 연구에서는 cross-corpus 병리 음성 분류에서 발생하는 성능 저하의 구조를 진단하고 대응 전략의 효과를 비교하기 위해 zero-shot transfer, multi-source 학습, target fine-tuning, 소규모 외부 사이트 검증을 포함한 실험 전략을 구성한다. 과적합 방지와 일반화 성능 향상을 위해 SpecAugment를 포함한 데이터 증강을 적용하였으며, 세부 학습 설정은 <표 4>와 같다. 5. 결과 단일 데이터 베이스에서의 결과는 위와 같다. 세 corpus 모두 UAR 0.728~0.837 수준으로 내부 분류 성능이 충분히 확보되었으며, 이는 이후 cross-corpus 성능을 판단하는 상한 기준으로 활용된다. Zero shot 전이 실패 양상 그런데 다른 데이터셋으로 그대로 옮기자 여섯 방향의 UAR이 0.504~0.603 , MCC가 0.013~0.209 로 내려갔다. 더 중요한 것은 어떻게 틀렸는가였다. SVD → AVFAD 의 UAR은 0.515였고, 병리 음성을 병리로 맞힌 민감도는 0.962였다. 이 숫자만 보면 병리 음성을 잘 찾아낸 것 같지만, 정상 음성을 정상으로 맞힌 특이도는 0.068 에 불과했다. 실제로는 거의 모두를 병리로 예측하는 쪽으로 기운 것이다. FEMH → AVFAD 도 민감도 0.922, 특이도 0.085로 비슷한 양상이었다. 반대 방향의 쏠림도 있었다. AVFAD → FEMH 의 UAR은 0.588이고 특이도는 0.897이었지만, 민감도는 0.279 였다. 정상 음성은 비교적 잘 구분하는 대신 병리 음성을 많이 놓친 셈이다. 같은 UAR 근처의 점수라도 어느 클래스를 놓치는지에 따라 실패의 모습이 전혀 달랐다. 이 차이를 보기 위해 예측한 병리 비율 - 실제 병리 비율 을 prediction bias 로 정의했다. 양수이면 병리 과다 예측, 음수이면 병리 과소 예측이다. 분석에서는 이 값이 커질수록 민감도는 높고 특이도는 낮아지는 경향이 확인됐다. 핵심은 점수 한 개가 떨어진 사실보다, 데이터셋이 바뀌며 예측이 한쪽 클래스로 기울었다는 것 이다. 이 편향을 이해하기 위해 임계값, 학습 데이터의 클래스 비율, 모델 내부 표현의 차이를 차례로 확인했다. 전이 실패 분석 전이 실패의 원인을 분리하기 위해 몇가지 진단을 수행하였다. 첫째, 결정 임계값 조정 첫 번째 가설은 0.5 라는 분류 임계값이 새 데이터셋에 맞지 않는다는 것이었다. 평가 데이터의 정답을 알고 있다고 가정하고 UAR이 최대가 되는 임계값을 찾아보니, SVD → FEMH 는 0.574에서 0.706 으로 올랐다. 분명 임계값 불일치의 영향이 있었다. 하지만 다른 조합에서는 개선 폭이 작았고, 임계값을 바꿔도 무너진 분류가 충분히 회복되지는 않았다. 또한 정답을 보고 고른 이 값은 원인을 확인하기 위한 상한 실험 이지, 정답이 없는 새 환경에서 그대로 선택할 수 있는 운영 임계값은 아니다. *둘째, 클래스 불균형을 통제하기 위한 클래스 비율(prior) 조정 * 두 번째 가설은 학습 데이터의 정상·병리 비율 차이였다. SVD와 FEMH의 학습 데이터를 1:1 비율로 맞춰 다시 평가했지만, UAR 개선은 조합에 따라 0.002~0.023 에 머물렀다. 클래스 비율은 일부 영향을 줄 수 있으나, 데이터셋 간 성능 저하를 혼자 설명하기에는 부족했다 셋째, source–target 간 centroid distance(CD)와 MMD를 계산하여 6개 조합에서의 거리–성능 상관을 탐색 음성에서 추출한 모델 내부 표현의 분포 차이도 살펴봤다. 데이터셋 간 임베딩 거리와 성능의 관계를 탐색했지만, 방향별 비교가 여섯 개뿐이고 클래스별 거리와 민감도·특이도의 관계가 일관되지 않았다. 언어·녹음·질환 구성 중 어느 요인이 주원인인지는 이 실험만으로 분리할 수 없다. 원인을 하나로 특정하기 어렵다면, 실제 성능을 회복하는 방법은 무엇일까? 학습된 모델에 소량의 타겟 데이터 fine-tuning 대상 데이터의 일부 정답을 학습에 쓸 수 있을 때는 상황이 달라졌다. 예를 들어 SVD → AVFAD 의 대표 실험에서 zero-shot UAR 0.520 이 대상 데이터 미세조정 후 0.820 으로 올랐다. SVD → FEMH 도 0.599 → 0.822 로 회복됐다. 마지막 분류층만 조정하는 것보다 앞쪽 표현까지 조정하는 설정이 유리한 경우가 있었다. 이는 차이가 단순히 최종 임계값이나 마지막 층에만 있지 않을 가능성을 보여준다. 외부 데이터에서 실패한 모델을 현지 데이터로 상당 부분 회복할 수 있었다 는 것이다. 이후 지금까지 졸업논문으로 제출한 cross - corpus 관련된 연구 요약이다. 이 연구를 진행하면서 novelty 있는 결과를 보여주지 못했지만 그 안에서 알게된 것들이 많다. 다음 게시물에서는 한국 AIHUB 음성장애 데이터 활용에 있어 발생한 여러 문제점 IRB 승인 과정 느낀 점 에 대해 작성할 예정이다.
Linux 프로세스·사용자 관리 정리 — Day 13 Day 13 — Process와 사용자 관리 1. Process 구조 Linux에서는 Parent Process가 fork() 를 통해 Child Process를 만들고, Child Process에서 execve() 를 통해 새로운 Program을 실행할 수 있다. Parent Process │ │ fork() / \ / \ / \ Parent Process Child Process │ │ │ execve() │ │ │ 새로운 프로그램 실행 │ │ wait() │ │ exit(status) │ │ │◀──── 종료 상태 전달 │ wait() 반환 │ 부모 실행 계속 Process 확인과 제어에 사용한 명령어는 다음과 같다. ps ps -ef pstree pgrep kill jobs fg %1 bg %1 ps -e : System의 모든 Process 출력 ps -f : 상세 정보 출력 pstree : Parent-Child 관계 확인 jobs : Background 작업 확인 fg / bg : Foreground와 Background 전환 2. Archive와 Compression 여러 File을 하나로 묶을 때 tar 를 사용한다. -c : 새로운 tar 파일 생성 -t : tar 파일의 내부 내용들의 리스트 확인 -x : tar 파일 해제 -k : 덮어씌움 방지 -f : 아카이브 파일이나 테이프 장치를 지정 -v : tar 명령어 수행과정 자세히 출력 -h : 아카이브하려는 파일이 심볼릭 링크인 경우 원본을 아카이브 -z : gz로 압축 진행 Compression에는 gzip , bzip2 , zip 등을 사용할 수 있다. gzip gunzip bzip2 bzcat zip unzip Archive는 여러 File을 하나로 묶는 것이고 Compression은 Data Size를 줄이는 과정이다. 3. 사용자와 Group 정보 사용자와 Group 관련 주요 File은 다음과 같다. /etc/passwd /etc/shadow /etc/group /etc/gshadow /etc/passwd : 사용자 정보 /etc/shadow : Password 정보 /etc/group : Group 정보 /etc/gshadow : Group Password 정보 Linux에서는 사용자 이름보다 UID 를 기준으로 Permission을 판단한다. 실습에서는 두 사용자에게 같은 UID를 설정했을 때 Linux가 동일한 사용자처럼 판단하는 것을 확인했다. nobreak@rocky-node1:~$ sudo useradd test11 nobreak@rocky-node1:~$ sudo usermod -u 1000 -o test11 nobreak@rocky-node1:~$ id test11 uid=1000(nobreak) gid=1002(test11) groups=1000(nobreak) 4. usermod와 userdel 사용자 설정 변경: -a : 기존 보조 그룹 유지하면서 추가 -L : 계정 잠금 -U : 계정 잠금 해제 Group을 추가할 때는 기존 보조 Group을 유지하도록 -aG 를 함께 사용하는 것이 중요하다. sudo usermod -aG admins student6 사용자 삭제: sudo userdel testuser Home Directory까지 함께 삭제: sudo userdel -r testuser -r 없이 삭제하면 사용자 계정은 사라져도 Home Directory는 남을 수 있다. 5. Group 관리 Group 생성: nobreak@rocky-node1:~$ sudo groupadd -g 989 apache Group 이름과 GID 변경: nobreak@rocky-node1:~$ sudo groupmod -n apache2 apache nobreak@rocky-node1:~$ sudo groupmod -g 3000 apache2 nobreak@rocky-node1:~$ grep apache2 /etc/group apache2:x:3000: Group 삭제: nobreak@rocky-node1:~$ sudo groupdel apache2 기본 Group으로 사용되고 있다면 바로 삭제할 수 없다. 6. /etc/skel /etc/skel 은 새로운 사용자의 Home Directory에 기본적으로 들어갈 File을 관리한다. 실습에서는 welcome.txt 와 .bashrc 설정을 추가했다. nobreak@rocky-node1:~$ sudo touch /etc/skel/welcome.txt nobreak@rocky-node1:~$ echo 'echo "Welcome $USER"' | sudo tee -a /etc/skel/.bashrc echo "Welcome $USER" 이후 새로운 사용자를 생성하였다. nobreak@rocky-node1:~$ sudo useradd student11 nobreak@rocky-node1:~$ sudo su - student11 Welcome student11 새 사용자에게만 /etc/skel 의 내용이 복사되며 기존 사용자에게는 자동 적용되지 않는다. 7. su와 sudo su 는 현재 Session에서 다른 사용자로 전환한다. su su - su - 는 Login Shell 형태로 전환하여 해당 사용자의 환경까지 새로 적용한다. sudo 는 허가된 사용자가 root 등의 권한으로 Command를 실행한다. 대표적인 sudo 설정은 /etc/sudoers 에서 확인할 수 있다. root ALL=(ALL) ALL %wheel ALL=(ALL) ALL # %wheel ALL=(ALL) NOPASSWD: ALL % 는 Group을 의미한다. 8. tee Root Permission이 필요한 File에 Redirection할 때 tee 를 활용할 수 있다. nobreak@rocky-node1:~$ echo "hello" | tee hello.txt hello nobreak@rocky-node1:~$ cat hello.txt hello sudo와 함께 사용할 수도 있다. echo "내용" | sudo tee /root/file Day 13 한 줄 정리 Linux에서는 Process를 생성·제어하고, UID·GID를 기반으로 사용자와 Group을 관리하며 sudo 를 통해 높은 권한의 Command 실행을 통제한다. Linux 고급 권한·작업 스케줄링 정리 — Day 14 Day 14 — 특수 Permission과 Scheduling 1. /etc/skel과 sudo Group 실습 새로운 사용자에게 공통 File을 제공하도록 /etc/skel 을 설정하였다. nobreak@rocky-node2:~$ echo "welcome" | sudo tee /etc/skel/welcome.txt welcome nobreak@rocky-node2:~$ sudo useradd student4 nobreak@rocky-node2:~$ sudo ls -l /home/student4/ total 4 -rw-r--r--. 1 student4 student4 8 Sep 29 00:35 welcome.txt .bashrc 에도 Login Message를 추가했다. nobreak@rocky-node2:~$ echo "echo 'hello'" | sudo tee -a /etc/skel/.bashrc echo 'hello' nobreak@rocky-node2:~$ sudo useradd -m student5 nobreak@rocky-node2:~$ sudo su - student5 hello student5@rocky-node2:~$ exit logout sudo를 사용할 admins Group도 구성하였다. nobreak@rocky-node2:~$ sudo groupadd admins nobreak@rocky-node2:~$ sudo visudo -f /etc/sudoers.d/admins nobreak@rocky-node2:~$ sudo cat /etc/sudoers.d/admins %admins ALL=(ALL) ALL nobreak@rocky-node2:~$ sudo useradd student6 nobreak@rocky-node2:~$ sudo usermod -aG admins student6 sudo 사용 기록은 다음 Log에서 확인했다. nobreak@rocky-node2:~$ sudo cat /var/log/secure 2. setuid 실습 일반 사용자는 /etc/shadow 를 읽을 수 없다. nobreak@rocky-node2:~$ mkdir perm_prac nobreak@rocky-node2:~$ cp /bin/cat ~/perm_prac/secret_cat nobreak@rocky-node2:~$ perm_prac/secret_cat /etc/shadow perm_prac/secret_cat: /etc/shadow: Permission denied File Owner를 root로 변경하였다. nobreak@rocky-node2:~$ sudo chown root perm_prac/secret_cat setuid를 설정하였다. nobreak@rocky-node2:~$ sudo chmod u+s perm_prac/secret_cat Permission은 다음과 같이 변경된다. -rwsr-xr-x. 1 root nobreak 69456 Sep 30 00:06 perm_prac/secret_cat 이후 일반 사용자로 실행해도 File Owner인 root의 Effective Permission으로 Program이 실행되어 /etc/shadow 에 접근할 수 있었다. 3. 특수 Permission Permission 기호 역할 setuid s 실행 시 File Owner 권한 사용 setgid s File에서는 Group 권한 사용, Directory에서는 Group 상속 Sticky Bit t 공유 Directory에서 다른 사용자의 File 삭제 제한 Sticky Bit 실습에서는 먼저 모든 사용자가 Write 가능한 Directory를 만들었다. nobreak@rocky-node2:~$ mkdir /tmp/testdir nobreak@rocky-node2:~$ chmod 777 /tmp/testdir/ user01 이 만든 File을 user02 가 삭제할 수 있었다. Sticky Bit 설정: nobreak@rocky-node2:~$ chmod +t /tmp/testdir/ nobreak@rocky-node2:~$ ls -ld /tmp/testdir/ drwxrwxrwt. 2 nobreak nobreak 6 Sep 29 01:26 /tmp/testdir/ 이후 다른 사용자의 File 삭제가 차단되었다. rm: cannot remove '/tmp/testdir/fileA': Operation not permitted 4. ACL 기본 Owner / Group / Others 구조에 포함되지 않는 특정 사용자에게 Permission을 줄 때 ACL을 사용할 수 있다. root@rocky-node2:~# setfacl -m u:user01:rw acl/files/file1.txt root@rocky-node2:~# ls -l acl/files/file1.txt -rw-rw-r--+ 1 root root 10 Sep 29 02:08 acl/files/file1.txt root@rocky-node2:~# getfacl acl/files/file1.txt # file: acl/files/file1.txt # owner: root # group: root user::rw- user:user01:rw- group::r-- mask::rw- other::r-- ACL의 mask 는 실제 적용할 수 있는 최대 Permission을 제한한다. user:user1:rw- #effective:r- group::r-- mask::r-- 5. crontab 반복 작업 Scheduling에는 crontab 을 사용한다. -e : crontab 파일 편집 -l : crontab 파일 내용 출력 -r : crontab 파일 삭제 Cron 구조: # .---------------- minute (0 - 59) # | .------------- hour (0 - 23) # | | .---------- day of month (1 - 31) # | | | .------- month (1 - 12) # | | | | .---- day of week (0 - 6) # | | | | | # * * * * * command 수업에서 사용한 예시는 다음과 같다. # 매일 02:00에 실행 0 2 * * * # 평일 09:00에 실행 0 9 * * 1-5 # 10분마다 실행 */10 * * * * # 2시간마다 실행 0 */2 * * * # 분기별 첫날에 실행 0 0 1 1,4,7,10 * # 5분 마다 현재 시스템의 메모리 사용량 확인 및 결과를 ~/memory_log.txt 파일에 추가 */5 * * * * free -h >> ~/memory_log.txt # 매일 오후 2시 30분에 홈 디렉토리의 .tmp 파일을 모두 삭제하는 작업 30 14 * * * find ~ -name "*.tmp" -type -f -delete 6. Anacron Cron 실행 시점에 System이 꺼져 있으면 작업이 누락될 수 있다. Anacron은 System이 다시 실행되었을 때 작업 주기를 확인해 누락된 작업을 실행할 수 있다. 1 5 cron.daily 7 25 cron.weekly @monthly 45 cron.monthly Day 14 한 줄 정리 setuid·setgid·Sticky Bit와 ACL을 이용해 기본 Permission보다 세밀한 권한을 설정하고, Cron과 Anacron을 이용해 반복 작업을 자동 실행할 수 있다. Linux Disk·LVM 정리 — Day 14 ~ Day 15 Day 14 — Disk와 File System 1. MBR과 GPT Disk의 Partition Table은 대표적으로 MBR과 GPT 방식으로 나뉜다. 구분 MBR GPT Firmware BIOS UEFI 특징 기존 Partition 방식 GUID 기반 Partition 방식 대용량 Disk 약 2TB 제한 매우 큰 Disk 지원 2. fdisk Partition은 fdisk 를 이용해 관리했다. root@rocky-node2:~# fdisk /dev/sdb 주요 Command는 다음과 같다. d delete a partition l list known partition types n add a new partition p print the partition table t change a partition type w write table to disk and exit q quit without saving changes g create a new empty GPT partition table o create a new empty MBR (DOS) partition table Partition 생성 후 XFS 등의 File System을 만들고 Mount하여 사용한다. Day 14 한 줄 정리 Disk는 Partition Table → Partition → File System → Mount 과정을 거쳐 Linux의 Directory 구조에 연결된다. Day 15 — LVM 1. LVM 구조 LVM은 다음 구조로 Storage를 관리한다. Partition ↓ PV ↓ VG ↓ LV ↓ File System ↓ Mount 2. LVM 생성 실습 Partition Type을 Linux LVM으로 변경하였다. root@rocky-node2:~# fdisk /dev/sdb Command (m for help): t Selected partition 1 Hex code or alias (type L to list all): 8e Changed type of partition 'Linux' to 'Linux LVM'. PV 생성: root@rocky-node2:~# pvcreate /dev/sdb1 Physical volume "/dev/sdb1" successfully created. VG 생성: root@rocky-node2:~# vgcreate vg_test /dev/sdb1 Volume group "vg_test" successfully created LV 생성: root@rocky-node2:~# lvcreate -n lv_test -L 5G vg_test Logical volume "lv_test" created. XFS 생성: root@rocky-node2:~# mkfs.xfs /dev/vg_test/lv_test Mount: root@rocky-node2:~# mkdir /lvmtest root@rocky-node2:~# mount /dev/vg_test/lv_test /lvmtest root@rocky-node2:~# df -h /lvmtest Filesystem Size Used Avail Use% Mounted on /dev/mapper/vg_test-lv_test 5.0G 130M 4.9G 3% /lvmtest 3. Logical Volume 확장 1GB File 생성: root@rocky-node2:~# sudo dd if=/dev/zero of=/lvmtest/testfile bs=1M count=1000 1000+0 records in 1000+0 records out 1048576000 bytes (1.0 GB, 1000 MiB) copied, 0.222304 s, 4.7 GB/s Logical Volume을 5GB에서 8GB로 확장하였다. root@rocky-node2:~# lvextend -L 8G /dev/vg_test/lv_test Size of logical volume vg_test/lv_test changed from 5.00 GiB (1280 extents) to 8.00 GiB (2048 extents). Logical volume vg_test/lv_test successfully resized. 하지만 File System은 아직 5GB로 인식하고 있었다. /dev/mapper/vg_test-lv_test 5.0G 1.2G 3.9G 23% /lvmtest XFS File System까지 확장하였다. root@rocky-node2:~# xfs_growfs /lvmtest/ 확인: root@rocky-node2:~# df -h | grep vg /dev/mapper/vg_test-lv_test 8.0G 1.2G 6.8G 15% /lvmtest 4. Snapshot Snapshot 생성: root@rocky-node2:~# lvcreate -L 1G --snapshot --name lv_test_snap /dev/vg_test/lv_test Logical volume "lv_test_snap" created. Snapshot 생성 후 원본에 File을 추가했다. root@rocky-node2:~# touch /lvmtest/snapshot.txt XFS Snapshot은 원본과 같은 UUID를 사용하므로 그대로 Mount했을 때 Error가 발생했다. 이를 무시하고 Mount: root@rocky-node2:~# mount -o nouuid /dev/vg_test/lv_test_snap /snap 원본과 Snapshot을 비교하였다. root@rocky-node2:~# ls -al /snap total 1024000 drwxr-xr-x. 2 root root 22 Sep 30 08:29 . dr-xr-xr-x. 20 root root 262 Sep 30 08:38 .. -rw-r--r--. 1 root root 1048576000 Sep 30 08:29 testfile root@rocky-node2:~# ls -al /lvmtest total 1024000 drwxr-xr-x. 2 root root 42 Sep 30 08:37 . dr-xr-xr-x. 20 root root 262 Sep 30 08:38 .. -rw-r--r--. 1 root root 0 Sep 30 08:37 snapshot.txt -rw-r--r--. 1 root root 1048576000 Sep 30 08:29 testfile Day 15 한
2학년때, 처음 인공지능개론을 배우면서 각 알고리즘에 적합한 Loss Function이 다르다는 것을 배웠다. 사실 그때는 수식을 외우기에 바빠 이해보다 암기 위주로 학습을 진행했던 것 같다. 최근 들어 교수님 앞에서 논문을 리뷰하는 시간이 있는데, 교수님께서 기초적인 개념에 관한 질문을 많이 하신다. 거기에 피상적인 답만 하는 내 모습이 부끄러웠고, 아직 기초가 부족하다는 생각이 들었다. 그렇기에 이번 학기 위클리 블로그는 머릿속에 파편화되어 있는 개념들을, 하나씩 정리해보는 기회로 삼으려 한다. 이번 블로그 주제는 자주 등장하는 Loss Function에 관한 글이다. Regression Task와 Classification Task에서는 왜 서로 다른 Loss Function을 사용할까? 1. Regression & Classification Regression Task 연속적인 값을 예측하는 작업 → 출력은 실수 범위 내 대표적인 Loss Function: MSE(Mean Squared Error), MAE(Mean Absolute Error) $$ MSE = \frac{1}{n}\sum_{i=1}^{n}(y_i-\hat{y}_i)^2 $$ 실제값과 예측값 차를 제곱한 후 평균을 낸 것이다. 즉 MSE는 실제값과 예측값이 얼마나 멀리 떨어져 있는지 계산 할 수 있다. Classification Task 주어진 입력을 여러 클래스 중 하나로 분류하는 작업 → 이산적인 클래스 레이블 분류 작업의 출력은 각 클래스가 정답일 확률 이다. (ex. class = [a,b,c]면 모델의 출력은 [0.1, 0.7, 0.2] 이며, 최종적으론 b로 예측) 대표적인 Loss Function: Cross-Entropy 계열의 Loss (이진 분류)Binary Cross-Entropy (다중 분류) Categorical Cross-Entropy Binary Cross-Entropy $$ BCE = -\frac{1}{n} \sum_{i=1}^{n} \left[ y_i\log(\hat{y}_i) + (1-y_i)\log(1-\hat{y}_i) \right] $$ 그런데 왜 Classification에서는 MSE가 아니라 Cross-Entropy를 사용하는 걸까? Entropy란 무엇일까? 2. Entropy란? 어떤 시스템이나 확률 분포의 불확실성 또는 정보의 불확실성을 나타내는 척도다. 쉽게 생각하면 어떤 사건의 결과를 얼마나 예측하기 어려운가 를 나타내는 값이다. 이산확률분포 $P$의 Entropy는 다음과 같다. $$ H(P) = -\sum_i P(i)\log P(i) $$ 예를 들어 동전의 앞면이 나올 확률이 100%면 결과가 이미 정해져 있으므로 Entropy는 0이다. 반대로 앞면과 뒷면의 확률이 각각 50%라면, 어떤 결과가 나올지 가장 예측하기 어렵기에 Entropy가 가장 크다. 그런데 식에는 왜 log 가 들어갈까? 정보이론에서는 어떤 사건 $x$가 발생했을 때 얻는 정보량을 다음과 같이 정의한다. $$ I(x) = -\log P(x) $$ 직관적으로 생각해보면, 확률이 높은 사건은 이미 예상하기 쉬운 사건이다. 따라서 실제로 발생해도 새롭게 얻는 정보가 많지 않다. 반대로 확률이 매우 낮은 사건이 실제로 발생하면 더 많은 정보를 얻게 된다. 예를 들어 $$ P(x)=1 $$ 이라면 $$ -\log 1 = 0 $$ 이다. 반드시 발생하는 사건이기 때문에 새로운 정보가 없는 것이다. 반대로 $P(x)$가 작아질수록 $-\log P(x)$는 커진다. Entropy는 이렇게 각 사건이 주는 정보량을 확률분포 전체에 대해 평균낸 값이라고 볼 수 있다. 3. Cross-Entropy 두 확률분포 $P$, $Q$에 대한 Cross-Entropy는 다음과 같다. $$ H(P,Q) = -\sum_i P(i)\log Q(i) $$ Classification에서는 $P$ = 실제 정답 분포 $Q$ = 모델이 예측한 분포 라고 생각할 수 있다. 예를 들어 실제 정답이 b 라면 $$ P = [0,1,0] $$ 이고, 모델의 예측이 $$ Q = [0.1,0.7,0.2] $$ 라고 해보자. Cross-Entropy를 계산하면 $$ -(0\log0.1 + 1\log0.7 + 0\log0.2) $$ 이므로 결국 $$ -\log0.7 $$ 만 남는다. 즉, 정답 클래스에 높은 확률을 줄수록 Loss는 작아지고, 낮은 확률을 줄수록 Loss는 커진다. 그렇다면 Cross-Entropy는 실제 분포와 예측 분포 사이의 차이를 측정하는 값이라고 볼 수 있을까? 여기서 조금 의문이 든다. 분포 간의 차이를 측정하는 방법으론 KL Divergence 나 Wasserstein Distance 같은 것도 있기 때문이다 4. Cross-Entropy와 KL Divergence KL Divergence는 두 확률 분포 간의 차이를 측정하는데 사용된다. $$ D_{KL}(P \parallel Q) = \sum_i P(i)\log\frac{P(i)}{Q(i)} $$ 식을 조금 풀어보면 $$ D_{KL}(P \parallel Q) = \sum_i P(i)\log P(i) - \sum_i P(i)\log Q(i) $$ 가 된다. 여기서 Entropy는 $$ H(P) = -\sum_i P(i)\log P(i) $$ 이고, Cross-Entropy는 $$ H(P,Q) = -\sum_i P(i)\log Q(i) $$ 이다. 따라서 $$ D_{KL}(P \parallel Q) = H(P,Q)-H(P) $$ 라는 관계를 얻을 수 있다. 다시 정리하면 $$ H(P,Q) = H(P)+D_{KL}(P \parallel Q) $$ 이다. 즉, Cross-Entropy = Entropy(실제 분포가 원래 가지고 있는 불확실성) + KL Divergence(두 분포의 차이) 라고 볼 수 있다. 그런데 모델을 학습할 때 실제 정답 분포 $P$는 이미 정해져 있으므로 $H(P)$는 고정된 값이다. 따라서 모델이 Cross-Entropy를 줄이는 것은, 결국 KL Divergence를 줄이는 것과 같다. 즉 Classification에서 Cross-Entropy를 최소화한다는 것은, 모델이 예측한 확률분포를 실제 정답 분포에 가깝게 만드는 과정이라고 이해할 수 있다. 정리하자면, 예측값이 실수인 회귀에서 사용하는 손실 함수 MSE 등은 실제값과 예측값 사이의 수치적인 차이 를 직접 계산할 수 있다. 반면 분류에서는 모델이 각 클래스에 대한 확률을 출력하고, Cross-Entropy를 최소화함으로써 모델이 예측한 확률분포를 실제 정답 분포에 가깝게 만든다! 처음에는 단순히 회귀면 MSE, 분류면 Cross-Entropy 정도로 암기했는데, 이 포스트를 작성면서 두 Task에서 서로 다른 Loss Function을 사용하는지 명확하게 이해할 수 있었다.
- 올것이 왔다. 복학하고 얼마 지나지 않아 코딩경진대회 개최 공지가 떴다. 이번에도 안보면 진짜 다음 시험은 4학년때나 볼 수 있어서... 한번 보기로 했다. PCCP 응시료를 공짜로 내주기도 하고. - 원래는 5만원이나 하는데 학과에서 대신 내준다. 산업기능요원 복무하는동안 회사에서 틈틈히 백준으로 코테 공부를 하긴 했는데, 잠시 휴학했던 사이에 백준은 역사속으로 사라져버렸고... PCCP를 주관하는 프로그래머스는 조금 다른 채점 방식을 사용한다. - solution이라는 메소드만 작성하면 된다. solution() 메소드만 작성해주면, input값을 그대로 가져와준다. 백준에서는 그 input시간도 줄여보겠다고 sys.stdin.readline을 활용했는데.. 그짓을 또 할 필요는 없어졌다. 솔직히 훨씬 편하다. 문제 읽으면서 코드 작성도 한 화면에서 할 수 있게 됐고. 여하튼, 오늘은 대회 준비하면서 풀었던 문제들 분석과 함께 대회 복기를 해보려 한다. PCCP 기출문제 1번 - 붕대 감기 일단 제일 먼저 문법에 익숙해지기 위해 풀었던 붕대감기 문제. 간단한 구현 문제고, 1번 문제인 만큼 난이도는 쉬웠다. def solution(bandage, health, attacks): answer=health now=0 for i,j in attacks: healtime=i-now-1 if answer==health: pass else: answer=min(health,(healtime//bandage[0])*bandage[2]+bandage[1]*healtime+answer) answer-=j now=i if answer<=0: return -1 return answer 대충 attacks 에 for문 돌려서, 각 공격마다 체력(answer)이 0 이하로 내려가면 바로 -1을 return하게 두고, 그렇지 않을 경우 최종적으로 남은 체력을 표시하게 하는 코드다. 문제를 제대로 읽어본다면 금방 풀 수 있는 문제. PCCP 기출문제 2번 - 석유 시추 2번 문제부터 바로 탐색을 시킨다... 머리로 알고 있어도 구현하기 진짜 귀찮은데... 늘 하던대로 ix[0,1,0,-1] iy[1,0,-1,0] 구현해주고 for문으로 상하좌우 탐색하게 시켰다. import sys sys.setrecursionlimit(1000000) def solution(land): m=len(land[0]) n=len(land) amountmap=[0] index=0 visited=[[-1]*m for _ in range(n)] where=[set() for i in range(m)] def searcher(x,y,code): ix=[1,0,-1,0] iy=[0,1,0,-1] visited[x][y]=0 for a in range(4): dx=x+ix[a] dy=y+iy[a] if 0<=dx<n and 0<=dy<m: if visited[dx][dy]==0: pass elif land[dx][dy]==0: visited[dx][dy]=0 pass elif land[dx][dy]!=0: visited[dx][dy]=0 where[dy].add(code) amountmap[code]+=1 searcher(dx,dy,code) for i in range(n): for j in range(m): if visited[i][j]==0: pass elif land[i][j]==0: visited[i][j]=0 pass elif land[i][j]!=0: amountmap[index]+=1 where[j].add(index) searcher(i,j,index) index+=1 amountmap.append(0) answer = 0 for i in where: num=0 for j in i: num+=amountmap[j] answer=max(num,answer) return answer 일단 연습문제는 다 맞았는데, 지도가 커지고 나면 탐색을 하면서 python 기본으로 내장된 재귀 제한 횟수를 넘기게 된다. 이 경우 재귀 제한을 더 늘려주는걸 잊지 말자. import sys sys.setrecursionlimit(1000000) 어쨋든, 각 석유를 발견할 때마다 발견한 순서대로 그 크기를 array에 따로 기록해두고, 석유에 code라는 이름의 시리얼 넘버를 기록한 뒤, set에 각 x 좌표별로 몇번 석유를 시추할 수 있는지 기록했다. 탐색하면서 동시에 솔루션까지 찾을 수 있으니, 이후에 한번 더 x 좌표별로 for문 돌리는것보다 시간 효율적이다. 마지막엔 X좌표별 Set를 하나씩 꺼내서, max값 비교만 해주면 되니까. PCCP 기출문제 3번 - 아날로그 시계 이번 문제는 친구가 풀다가 도저히 못풀었다고 해서 풀어봤다. 구현의 난이도는 그렇게까지 높진 않은데, 문제는 엣지 케이스가 너무 많다;;; def solution(h1, m1, s1, h2, m2, s2): Oclock=h2-h1 answer=0 if h1==h2 and m1==m2: if (h1==0 and m1==0 and s1==0) or (h1==12 and m1==0 and s1==0): return 1 if float((h1%12)*5)+float(m1)/12>=float(s1) and float((h2%12)*5)+m2/12<float(s2): answer+=1 if float(m1)+float(s1)/60>=float(s1) and float(m2)+float(s2)/60<float(s2): answer+=1 return answer totalmin=(h2*60+m2)-(h1*60+m1) if (h1==0 and m1==0 and s1==0): Oclock+=1 totalmin-=1 elif (h1==12 and m1==0 and s1==0): Oclock+=1 totalmin-=1 if s1>0: totalmin-=1 if float((h1%12)*5)+float(m1)/12>=float(s1): answer+=1 if float(m1)+float(s1)/60>=float(s1): answer+=1 elif s2>0: if float((h2%12)*5)+m2/12<float(s2): answer+=1 if float(m2)+float(s2)/60<float(s2): answer+=1 answer+=max(totalmin,0)*2 answer-=max(Oclock,0) return answer 그러니까, 1시간동안 초침은 분침과 시침을 60번 만나고, 자정과 정오의 경우 59번 만난다. 그래서 문제에서 주어진 시간을 풀타임 N시간+(m분 k초)로 구현했는데.. 생각보다 엣지케이스가 많았다. 엣지케이스 다 걸러내는 작업 하고 나서도 몇문제 틀리길래, GPT한테 도와달라고 했다. 자존심 상하긴 했지만, 엣지케이스를 구분할 수 있는것도 실력이라고 생각한다...