『リーダブルコード』第3章を読んで、誤解されない名前をつける
AIタグが付けられた新着記事 - Qiita
『リーダブルコード』第3章のテーマは、良い名前の第一条件は曖昧さがないことだ、という話。読み手の立場に立ち、どんな誤解が起こりうるかを先に想像してから名前を決める。 これまでの読書感想はこちら: 第3章は8つの節に分かれていて、それぞれ実際のコーディング場面を扱...
Балл: 57.4Уверенность: 54%
ПодробнееЗагружаем каталог…
НАВИГАТОР ПО ВОЗМОЖНОСТЯМ ИИ
Найдите свой ИИ-инструмент. Бесплатный доступ, пробные периоды и кредиты — в одном месте.
AIタグが付けられた新着記事 - Qiita
『リーダブルコード』第3章のテーマは、良い名前の第一条件は曖昧さがないことだ、という話。読み手の立場に立ち、どんな誤解が起こりうるかを先に想像してから名前を決める。 これまでの読書感想はこちら: 第3章は8つの節に分かれていて、それぞれ実際のコーディング場面を扱...
Балл: 57.4Уверенность: 54%
ПодробнееAIタグが付けられた新着記事 - Qiita
はじめに テキストを読んで、そのまま自由文を生成するだけではなく、分類や採点などの「判断」をアプリケーションから扱いたい場面があります。問い合わせの振り分け、優先度付け、次のアクションの選択などでは、回答を一定の形で受け取れることが重要です。 この記事では、意思決定モデル...
Балл: 57.39Уверенность: 54%
ПодробнееAIタグが付けられた新着記事 - Qiita
Java Silverに合格。Claude Code製の教材だけでは足りなかった話 はじめに 新卒2年目、現場でJavaを書いているエンジニアです。今期の目標のひとつとして、Oracle Certified Java Programmer, Silver SE 17(1...
Балл: 57.38Уверенность: 54%
ПодробнееAIタグが付けられた新着記事 - Qiita
「Physical AI」という言葉だけが先行し、その中身が見えにくくなっています テキスト、画像、音声、動画で大きく進歩したAIに対して、「世界がどう変化するか」「自分が行動すると何が起きるか」を扱う世界モデルについて、日本語でまとまった情報はまだ多くありません。そ...
Балл: 57.38Уверенность: 54%
Подробнее掘金
第一次看到 Rust 的生命周期标注,很多人都会有一个直接反应: 这里的 'a 到底是什么?它是不是在告诉 Rust,让这两个字符串“活久一点”?如果只是为了让引用活得更久,为什么不让程序自动处理?
Балл: 57.29Уверенность: 54%
Подробнее掘金
B站 173 个投稿活动看到眼瞎?我写了个扩展,主打一个精准打击 一、起因:教练,我想拿奖金 先说个扎心的事实:B站活动中心的列表页,是真的长。 173 个活动,从「夏日美食季」一路排到「某某品牌联名
Балл: 57.25Уверенность: 54%
ПодробнееReadhub
国际能源署今年 3 月宣布计划释放 4 亿桶战略石油储备,目前已释放约 3.25 亿桶,占承诺总量的 80% 以上。国际能源署署长法提赫・比罗尔在七国集团领导人视频会议上表示,该举措在填补供应缺口、稳定市场信心方面发挥了重要作用。
Балл: 56.93Уверенность: 54%
Подробнееvelog
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)에서 최신 내용을 한 번 확인하시길 권합니다.
Балл: 54.4Уверенность: 49%
Подробнееvelog
이번주에는 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 구조를 이해하는 데 도움이 될 것 같습니다.
velog
在我們的睡眠中, 夢境 猶如一扇通往潛意識的大門。當 女性 夢見墳頭,這畫面可能會讓人感到不安或疑惑。不同的夢境細節可能代表不同的意義,了解這些預兆不僅能滿足我們的好奇心,也可能為我們的生活提供一些啟示。接下來,就讓我們一起深入探討女性夢見墳頭的各種狀況。 夢見普通墳頭有什麼寓意? 象徵結束與新生:在周公解夢中,墳頭常象徵一段舊時光或舊事物的結束。對女性來說,這可能意味著人生某個階段的終結,例如一段感情的結束、一份工作的離職等。而結束往往也伴隨著新生,這或許預示著新的機會即將到來,就像經歷寒冬後迎來春天,是一個重新開始的好兆頭。 提示內在情緒:夢見墳頭也可能反映出女性內心的一些情緒。如果近期心事重重、壓力較大,墳頭可能是這種壓抑情緒的體現。它提醒女性要專注於自己的內心感受,及時調整心態,給自己的心靈放個假。 夢見自己走進墳地是吉是兇? 勇敢面對挑戰:若女性夢見自己走進墳地,這可能代表 增加女性敏感度 、 治療女性性冷淡 、 誘發女性性渴望 、 事後無記憶 、 迷幻催情春藥 她有勇氣面對生活中的難題。墳地通常被視為神秘且令人敬畏的地方,走進它意味著女性願意深入探索未知,克服內心的恐懼。這是一種正面的象徵,預示著她在現實生活中會有足夠的勇氣去解決困難。 注意潛在危險:不過,從另一個角度看,走進墳地也可能暗示女性正處於一個相對危險或複雜的環境。這可能是工作上的競爭、人際關係的困擾等。她需要保持警惕,小心應對,避免陷入不必要的麻煩。 夢見親人的墳頭意味著什麼? 對親人的思念:夢見親人的墳頭,很可能是女性對逝去親人的思念之情在夢中的體現。這種夢境可能會讓她感到悲傷,但也是一種情緒的宣洩。它提醒女性要珍惜身邊還健在的親人,多花時間陪伴他們。 獲得精神指引:在某些文化中,夢見親人的墳頭也被認為是親人在給予精神上的指引。女性可能會在夢中收到親人的訊息或啟示,或許能幫助她在現實生活中做出正確的決策。 夢見新墳是好還是壞? 財運方面:夢見新墳,在財運上可能有新的轉機。新墳代表著新的開始,這可能暗示著女性會有新的賺錢機會出現。但要注意把握機會,不要盲目跟風,以免造成損失。 感情方面:在感情上,夢見新墳可能預示著新的感情即將開啟。對於單身女性來說,這可能是邂逅真愛的好時機;而對於有伴侶的女性,可能意味著與伴侶的關係會有新的發展。 夢見墳頭著火了有何預兆? 事業運勢上升:夢見墳頭著火,從事業角度來看,是個正面的預兆。火代表著活力和熱情,這可能意味著女性在工作上會有新的動力和衝勁,能夠取得不錯的成績,獲得領導的認可和同事的讚賞。 感情升溫:在感情方面,墳頭著火可能像徵感情的熱度增加。 女性催淫春藥 、 激發女性性慾 、 增強性快感 、 增加女性主動 、 增加女性分泌物 夫妻之間可能會更加恩愛,情侶之間的感情也會更加甜蜜。但也要注意避免因為過於熱情而產生矛盾,要互相理解和包容。 不同狀態女性夢見墳頭有差嗎? 孕婦夢見墳頭:孕婦夢見墳頭,可能代表新生命即將到來的特殊預示。墳頭象徵舊的結束和新的開始,而孕婦正孕育著新的生命,這可能是潛意識裡對新生命誕生的一種反應,通常是比較好的預兆 戀愛中的女性夢見墳頭:戀愛中的女性夢見墳頭,可能意味著這段關係即將面臨一些改變。這改變可能是好的,例如感情比較穩定,也可能是需要雙方共同解決一些問題,以維持感情的長久。 工作女性夢見墳頭:工作女性夢見墳頭,可能預示工作上會有一些調整或改變。這可能是晉升的機會,也可能是面臨新的挑戰,需要她做好應對的準備。 女性夢見墳頭的預兆因夢境的具體細節和自身狀態的差異而有所差異。它可能像徵著結束與新生、反映內心情緒、預示財運和感情的變化等。但要注意的是,夢境的解讀只是一種參考,不能完全決定現實生活。
Балл: 54.4Уверенность: 49%
Подробнееvelog
이번 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까지 직접 구현해보면서 비동기 데이터가 화면에 표시되기까지의 흐름을 조금씩 이해할 수 있었다.
velog
클린코드 클린 코드는... 코드 스멜이 없고 가독성 이 높아 읽고 이해하기 쉬운 코드 단순해서 변경 및 확장 이 용이한 코드 의도 가 명확히 드러나 동료가 신뢰할 수 있는 코드 유연하고(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 원칙에 의한 것이었음을 알게 되어서 비로소 이해가 되는 기분이다. 그동안 생각해왔던 무조건 짧고 간결한 코드!가 아니라 코드는 길어보일지라도 실제 기능에 따라 쪼개고 더 이해하기 쉽게 만드는 게 훨씬 중요하다는 걸... 어찌 보면 너무나 당연한 걸 이제야 제대로 실감해버렸다. 아직도 코드를 쓰는 것은 너무 어렵게 느껴지지만, (일단 돌아가는 것에 급급하고... 늘고 있는지도 모르겠지만 ᅲᅲ) 내가 추구해야 하는 방향성에 대해서 알게 되어서 정말 유익했다고 생각한다.
Балл: 54.4Уверенность: 49%
Балл: 54.4Уверенность: 49%
Балл: 54.4Уверенность: 49%