동현의 나불나불
백엔드
배포
docker, AWS, github action을 활용한 배포
github_actions_기초
prometheus_이론
kafka 공부
ch1.kafka 시작하기
언어
자바
java 인터페이스
java다형성
java예외
java추상화
java컬렉션
javaThread
프로젝트
바로그림
django-학습-핵심-정리
elasticsearch-검색과-데이터-파이프라인-정리
활용한 기술
바로그림-프로젝트-설계와-시행착오
회고록
백엔드_통합점검
오늘의 식사
Today I Learned
2026_04_15~2026_04_19
2026_04_15
2026_04_16
2026_04_17
2026_04_20~2026_04_27
2026_04_20
2026_04_21
2026_04_22
2026_04_23
2026_04_24
2026_04_27~2026_05_01
2026_04_27
2026_04_28
2026_04_29
2026_04_30
2026_05_01
2026_05_04~2026_05_08
2026_05_04
2026_05_05
2026_05_06
2026_05_07
2026_05_08
2026_05_11~2026_05_15
2026_05_11
2026_05_12
2026_05_13
2026_05_14
2026_05_15
2026_05_21~2026_05_22
2026_05_21
2026_05_22
2026_05_27~2026_05_30
2026_05_27
2026_06_TIL
2026_06_11
2026_06_15
2026_06_16
2026_06_22
2026_06_25
2026_06_26
2026_06_28
2026_07_TIL
cs
네트워크
네트워킹 하향식 접근
http 프로토콜
데이터베이스
DB_SQL기초
Lock_diffrent
웹
os
chapter1 운영체제 서론
chapter2 운영체제 구조
chapter3 프로세스 관리
Home
Contact
Copyright © 2024 |
Yankos
Home
>
Today I Learned
> 2026_04_15~2026_04_19
Now Loading ...
2026_04_15~2026_04_19
2026-04-17 정리본
제네릭 구현 타입을 나중에 확정시키기 위해서 사용한다. 만약 Object의 기능을 대신해주고 그 기능을 수행할 수있도록 선언해주는 것이다. public class 클래스명 <문자> { public 문자 field1 public 문자 field2 } 또한 geneiric은 여러개의 문자를 사용하는 것이 가능하다. 우리가 클래스를 사용할 때는 하나의 타입이 아닌 여러 타입을 사용하게 되고 클래스의 타입을 명시를 해야한다. 그 중 컨테이너 객체는 객체를 감싸는 기능을 수행한다. 이 경우 generic을 구현해주면 타입을 체크해주고 꺼낼 때 타입을 보장해줘서 유용하게 사용가능하다. 제네릭 제한 public class 클래스명 <K extends Number, V, T> { public 문자 field1 public 문자 field2 } 제네릭을 사용하면 유용하게 대처할 수 있는 경우도 많지만 오히려 그 범용성 때문에 쓸 수 있는 메서드 수가 줄어들 수 있다. 그리고 단일 책임 원칙(Single Responsiblility Principle)을 지킬 수 있어서 유지보수성을 높이는 쪽에 도움이 된다. 이를 위해 타입에 대한 제한을 두어 해당 타입이 제공하는 기능을 안전하게 보장할 수가 있다. 함수형 인터페이스 프로그램의 진행에 대한 추적과 새로운 값을 불변성을 위해 도입하기 위해서 함수가 필요한데 java는 메서드만 있지 함수는 없다. 이를 위해 함수형 인터페이스의 도입으로 함수형 프로그래밍을 구현한다. //함수형 인터페이스 구현 void main(){ ABC a = () -> { IO.println("A"); }; a.a(); a.b(); } public interface ABC{ void a(); default void b(){ IO.println("B"); } } 제어의 역전(Inversion Of Control, IoC)과 라이브러리, 프레임워크의 관계 라이브러리는 주체가 사용자가 되어 내가 필요할 때 호출하거나 전체 흐름을 설계하는 것이 가능하다. 프레임워크는 IoC를 기반으로 동작하여 주체가 프로그램이 된다. 이 경우 결합도가 감소되어 인터페이스 중심의 설계가 가능해지고 코드 변경 영향을 최소화 하는 것이 가능하다. DAO, 아키텍쳐 개념 아키텍처는 객체들을 어떻게 나누고 어떻게 협력하게 만들지에 대한 설계구조이다. 대표적인 객체지향 아키텍쳐에는 3계층이 존재한다. Controller : 요청/응답 처리 Service : 비지니스 로직 DAO/Repository : DB 접근 이렇게 책임을 계층별로 분리하는 것이 아키텍쳐를 사용하는 핵심이라고 볼 수가 있다. 그 중 DAO가 지금 배우고 있는 jdbc와 큰 관계가 있는데 DAO는 데이터베이스의 데이터에 접근하고 조작하는 로직을 전담하는 객체이다. jdbc 코드 만들기 앞에서 배운 내용을 토대로 기본적인 Jdbc 예제를 만들어보자 public class JdbcTemplate { // 공통된 부분은 여기서 처리 // 다른 부분만 받기 private final DataSource dataSource; public JdbcTemplate(DataSource dataSource){ this.dataSource = dataSource; } Datasource 객체를 만들고 선언하여 HikariCP와 같은 커넥션 풀을 연결을 한번만 선안하기 위해서 사용한다. Datasource를 외부에서 주입하여 IoC를 구현하였다. 그리고 이 템플릿은 JDBC를 통한 DB 작업흐름만 다루고 있기 때문에 단일 책임 원칙을 만족한다. //단건조회 public <T> Optional<T> queryForOptional( String sql, StatementPlaceHolderBinder pss, RowMapper<T> mapper ){ List<T> results = queryForList(sql, pss, mapper); if(results.isEmpty()){ return Optional.empty(); } if(results.size() > 1){ throw new RuntimeException("Too many results"); } return Optional.of(results.getFirst()); } Optional으로 결과를 감싸주어서 nullPointException을 방지했고 결과를 0건, 1건, 2건이상을 다루었다. 0건인 경우 null값 방지를 위해 Optional의 empty() 메서드를 다루었고 2건이상인 경우 오류메시지를 반환하도록 하였다. getFirst()를 통해 1건만 반환하도록 했다. public <T> T queryForObject( String sql, StatementPlaceHolderBinder pss, RowMapper<T> mapper ){ List<T> results = queryForList(sql, pss, mapper); if(results.isEmpty()){ throw new RuntimeException("No Result!"); } if(results.size() > 1){ throw new RuntimeException("Too many results"); } return results.getFirst(); } 이 메서드는 앞의 queryForOptional과 다르게 무조건 1건만 반환하고 아니면 예외를 던지는 메서드이다. 이를 사용해주는 것은 Primary key와 같이 반드시 존재해야만 하는 값을 조회힐 때 사용가능하다. 로그인이나 PK조회와 같은 기능에서 Optional은 null값에 대해 흐름을 차단하지 않아 데이터 무결성을 깨트릴 우려가 있기에 이렇게 분류해서 사용하는 것이 필요하다. // 다건조회를 처리하는 메서드 public <T> List<T> queryForList( String sql, StatementPlaceHolderBinder pss, RowMapper<T> mapper ){ try( Connection connection = dataSource.getConnection(); PreparedStatement stmt = connection.prepareStatement(sql); ){ //select * from product where name = ? if(pss != null){ pss.set(stmt); } try (ResultSet rs = stmt.executeQuery()) { List<T> result = new ArrayList<>(); while(rs.next()){ result.add(mapper.map(rs)); } return result; } }catch (SQLException e){ throw new RuntimeException(e); } } try에 connection, PreparedStatement 객체를 넣어 자동 close를 하도록 코드를 구성했다. preparedStatement를 활용하여 sql의 ? 부분에 값을 넣어 진행하였다. rs의 커서가 빈 곳을 가리킬 때까지 반복하여 result값을 구현했다. public Long insertAndReturnKey( String sql, StatementPlaceHolderBinder pss ) { try( Connection connection = dataSource.getConnection(); PreparedStatement stmt = connection.prepareStatement(sql); ){ if(pss != null){ pss.set(stmt); } stmt.executeUpdate(); try(ResultSet rs = stmt.getGeneratedKeys()){ if (rs.next()) return rs.getLong(1); return null; } }catch (SQLException e){ throw new RuntimeException(e); } } } 데이터를 DB에 Insert를 하고 DB가 생성해준 key값을 반환하는 기능을 수행한다. stmt.executeUpdate로 pss에 대해 업데이트를 사용해준다.
Today I Learned
· 2026-04-16
2026-04-16 정리본
앞으로도 많이 사용할 IntelliJ 단축키 단축키 세미콜론(;) 넣기 : ctrl alt enter 코드 이동 : alt shift 방향키 매서드 추출 : ctrl alt m 변수 추출 : ctrl alt v 함수 매개변수 확인 : ctrl p shift f6 : 변수와 이름이 같은 모든 것을 바꿀 수가 있다. alt shift : 여러개 클릭 psvm : 메인함수 정의 sout : 출력함수 출력 JDBC 활용 기본적인 JDBC쿼리 //select문을 실행 stmt.executeQuery("select * from mytable") //insert, update, delete문 실행 stmt.executeUpdate("insert into mytable values ('string_val', 'float_val')"); // query resultSet으로 query의 실행문을 받아 // while로 next로 각 column에 맞는 값을 입력받아 진행 ResultSet multipleResultSet = multipleResultStmt.executeQuery(); // Iterator while(multipleResultSet.next()) { long id = multipleResultSet.getLong("id"); String name = multipleResultSet.getString("name"); int price = multipleResultSet.getInt("price"); int stock = multipleResultSet.getInt("stock"); System.out.printf("%d번 상품 %s, 가격: %d, 재고: %d\n", id, name, price, stock); } Result 출력 ResultSet 객체는 데이터베이스 쿼리의 결과 집합을 나타낸다. 이때 ResultSet은 커서 방식을 사용하는데 내부에 커서를 두고 이동하게 된다. 이때 커서는 단방향으로 움직이게 된다. 이는 DB나 드라이버 내부 구현이 단순해지고 트랜잭션/커넥션 유지 부담이 감소해서 그렇다. //ResultSet private static int getStock(Connection conn, Long productId) throws SQLException { String sql = "select stock from product where id = ?"; try(PreparedStatement stmt = conn.prepareStatement(sql)){ stmt.setLong(1, productId); try(ResultSet rs = stmt.executeQuery()){ return rs.next() ? rs.getInt("stock") : 0; } } } 주의 SQL injection 정의 : 임의의 SQL문을 주입하고 실행되게 하여 취약점 침입을 하는 것 SQL injection 예시 select * from member where email = 'user@email'and password = '' OR '' = ''; 위와 같이 비밀번호를 TRUE로 만들어주는 값을 대입하여 사용자 정보를 탈취하는 것이 가능하다. 이에 대한 기본적인 보안으로 jdbc에서는 prepaerStatement 기능을 제공하고 있다. prepareStatement SQL쿼리의 구조를 미리 정의하고 사용자 입력 값을 파리미터로 처리 이 경우 DB가 값을 데이터로만 처리하고 SQL코드로는 인식을 하지 않는다. String sql = "SELECT * FROM users WHERE id = ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, "user1"); ResultSet rs = ps.executeQuery(); prepareStatement를 사용하게 되면 SQL구조가 먼저 DB에 도달하게 되고 DB가 쿼리 파싱, 컴파일을 거치고 ?에 값만 나중에 바인딩이 된다. try ( Connection conn = ds.getConnection(); PreparedStatement ps = conn.prepareStatement(sql) ) { ps.setString(1, userInput); ResultSet rs = ps.executeQuery(); } 이런 식으로 setString() 메서드로 값을 넘긴다. PreparedStatement preparedStatement = conn.prepareStatement(safeLoginQuery); preparedStatement.setString(1, user1Email); preparedStatement.setString(2, user1Pwd); ResultSet safeResultSet1 = preparedStatement.executeQuery(); 그리고 넘긴 답을 바탕으로 executeQuery() 메서드를 통해 실행을 하게된다. 커넥션 풀 배경 : JBDC 연결은 드라이버를 로드하고 연결하여 객체를 받아와야 하는 과정을 거치게 되는데 이때 되게 많은 시간과 자원소모에 대한 단점을 가지게 된다. 개념 : 최초 Pool 내에서 Connection들을 하여 HTTP 요청에 따라 응답을 제공하고 반환받아서 이를 재사용하는 것 과정 애플리케이션이 실행되면서 POOL 내에 connection들을 생성해둔다. HTTP의 요청이 올 때 Pool 내에서 Connection 객체를 가져다 사용 사용이 완료된 Connection 객체는 Pool 내에 반환된다. DB가 동시에 처리할 수 있는 적절한 커넥션 개수 (코어 수 × 2) + 유효 디스크 스핀들 수 (SSD 환경: 코어 × 2 + 1) hikariCP 고성능, 경량 JDBC 커넥션 풀을 제공하는 오픈소스라이브러리 커넥션 풀이 설정된 커넥션의 사이즈만큼 연결을 허용하며 HTTP 요청에 대해 순차적으로 DB 커넥션을 처리주는 기능 수행 HikariConfig config = new HikariConfig(); config.setMaximumPoolSize(10); // 최대 커넥션 수 config.setMinimumIdle(5); // 최소 유지 커넥션 config.setConnectionTimeout(30000); // 대기 시간 팁 static 코드 블록을 사용하는 이유 static { HikariConfig config = new HikariConfig(); config.setJdbcUrl(JDBC_URL); config.setUsername(USERNAME); config.setPassword(PASSWORD); config.setDriverClassName(DRIVER_CLASS_NAME); hikariDataSource = new HikariDataSource(config); } static 코드 블록은 자바 클래스 로딩 시 딱 한 번만 시행되는 초기화 코드로 커넥션 풀을 애플리케이션 시작 시 1번만 생성하는 것이 가능하다. 이는 싱글톤 초기화에 적합하고 HikariCP는 DB연결 생성 시 생성 비용이 존재, 내부적으로 커넥션 풀을 관리, 애플리케이션 전역에서 공유를 하기 때문에 적합하다고 볼 수가 있다. 트랜잭션에서 finally setAutoCommit(true)를 넣는 이유 try{ }catch (Exception e){ connection.rollback(); }finally{ connection.setAutoCommit(true); } 이는 커넥션 풀 환경에서 필수적인 안전장치이다. 다음 사람이 같은 커넥션을 쓸 때 트랜잭션을 꼬이지 않게 하기 위해서 필요하다 만약 임의의A 사용자가 커넥션을 가져간 상태라면 autoCommit이 false로 변경된다. 작업이 끝났는데 커넥션을 반납했는데 autoCommit을 안돌렸다면 그 커넥션을 가져간 다음 사용자 B는 수동 트랜잭션 상태에서 시작하게 된다. 이 경우 사용자 B는 commit()을 안하면 데이터가 반영안되고 lock, 예상치 못한 rollback이 생길 수도 있다. scope 공부 Scope란? 변수나 메서드, 클래스가 어디서부터 어디까지 접근 가능한지 정의하는 범위 백엔드 개발에서 멀티 스레드를 통한 여러 요청을 동시 처리, 메모리 관리 등 직무와 크게 연관있기 때문에 필수적인 역량이다. 이는 객체의 생명주기와 공유 범위를 결정하고 멀티스레드 환경에서 데이터 정합성과 성능에 직접적인 영향을 주는 요소이다. 종류 클래스 스코프 클래스 스코프는 클래스 전체에서 접근할 수 있는 변수나 메소드의 범위를 의미 메서드 스코프 메소드 내에서 선언된 변수들이 가지는 범위 메소드가 종료될 때 소멸된다. 메소드 내부에서 접근 가능하며, 메소드가 호출될 때마다 새롭게 생성 블록 스코프 {}로 묶인 코드 블록내에서 선언된 변수들이 가지는 범위 변수들은 해당 블록 내에서만 유효, 블록을 벗어나면 소멸 scope 잘 모르면 터질 수 있는 문제 static 남용 static int count = 0; 모든 요쳥에 공유되고 이로 인해 여러 스레드를 동시에 수정해야 할 수도 있다. race condition을 발생시킬 우려가 있다. 지역변수 vs 인스턴스 변수 혼동 public class UserService { private User user; public void save(User u) { this.user = u; } } 만약 싱글톤 객체를 불러오는 경우 여러 요청이 같은 user을 공유할 때 데이터가 섞일 우려가 있다. 하나일 때는 별문제가 없어 보일 수도 있지만 여러 사용자가 동시에 요청하는 경우 데이터가 섞여져서 데이터 오염이 된다. 참고로 이런 멀티쓰레드 문제를 synchronized로 해결을 하려고 하면 Bottleneck이 생길 우려가 있어 권장하지 않는다. 메모리 누수 필요 없는 객체를 넓은 scope에 두어 사용하게 되면 요청마다 데이터가 추가되게 된다. 이 경우 java의 GC(Garbage Collector)가 수거를 못하게 되어 메모리가 계속 쌓이게 되고 서버 메모리가 증가해 서버가 느려지게 된다.
Today I Learned
· 2026-04-16
2026-04-15 정리본
Git 다루기 github 블로그 첫글입니다. Git 버전 제어 체계(vcs) 버전 : 특정 상태에 대한 일정한 규칙에 따른 일련번호 형상 관리 시스템 s/w 생명주기 기획 설계 구현 (기능 추가, 성능개선, 버그 수정) , 검사, 배포, 유지보수 여기서 버전 관리도 되게 중요하다. 호스팅 어떠한 서비스를 고객들에게 제공해주는 행위 git을 호스팅하는 프로그램들 예시 Bitbucket Github Gitlab 지역 저장소(local Repository) git.init, 저장할 로컬 리포지토리가 프로젝트 디렉토리 안에 생긴다. 프로젝트 폴더 내에서 실행을 해야한다. 바로는 안보인다. 숨김파일로 생성이 되기 때문이다. 실수로 삭제하지 말라고 숨김 파일로 생성이 된다. 💡 git commit 시 업로드 되는 파일을 적절하게 조절하는 것도 하나의 과정이다. 로컬에서의 Commit git은 commit을 받아 스테이지에 저장 → 하나의 버전이 생긴다. 이때 git은 원격 리포지토리에 저장하는 것이 아니고 로컬 repository에 저장을 하게 된다. 버전 저장하기 git commit -m "MESSAGE" 협업을 할때 처음 결정하는 것 커밋 메세지 어떻게 작성을 할 것인지? —> 커밋 컨벤션 참고하는 편이 좋다. —> 커밋 규칙을 세워두고 시작하자 EX) feat, 회원 가입가능, fix, bug, chore과 같이 앞에 어떤 말을 정해서 알려주는 것이 좋다. 로컬에서 커밋 히스토리가 진행되는 순서 로컬 리포지토리에 커밋 히스토리가 기록된다. 커밋 하나당 커밋 메시지가 생기게 된다. 커밋 내역을 확인하고 이전 버전으로 돌아간다. Branch git branch <branch name> checkout git checkout <branch name> 원격 저장소 로컬을 업로드 할 원격 레포지토리를 만든다. origin 대신에 [별칭][URL] 을 사용할 수가 있는데 이 이유는 하나의 로컬 레포지토리에 여러 원격지 레포지토리를 두는 것이 가능하기 때문이다. 💡 자주나오는 실수 원격 레포지토리와 로컬 레포지토리의 히스토리가 다른 경우 충돌이 발생할 수가 있다. git push [별칭] [브랜치이름] push를 할 때 현재 head가 가리키고 있는 브랜치에 시도를 해야한다. 로컬 레포지토리에 작성되어 있는 버전을 원격 레포지토리에 업로드를 하는 것 #branch 생성하기 git branch #branch 변경하기 git checkout -b branch git clone #스테이지의 현재상태 확인 git clone [https: ~~~~ ] git merge #스테이지의 현재상태 확인 git merge [다른 브랜치 이름] 참고 github 들어갔을 때 설명하는 markdown은 잘 정리되어 있고 흥미를 돋구는 내용으로 작성할 것 표시 : 인용구라는 의미 자주 일어날 수 있는 문제 사항 git 자체는 뭐가 최신인지 이전 것인지를 구분하지 못한다(ambigious) → 사용자가 정해줄 필요가 있다. 이것을 못 푸는 경우 일어나는 것이 충돌(conflict) 첫번째는 없던 문장이 추가되는 것이니 큰 문제가 아니지만 branch가 여러 개일 때 똑같은 코드를 여러명이 만질때 해결(resolve)를 해줘야 한다. git rebase 로컬과 원격이 버전이 많이 차이나는 경우 리베이스는 → 두 개로 나눈 브랜치를 하나로 뭉게서 합치는 것, 시간 순서로 계산한다. git rebase -i 장점 : 히스토리를 하나로 합칠 수가 있다. 단점 : 잘못된 병합에서 잘못된 점을 해결하기 힘들 수가 있다. 이 경우 히스토리가 일렬로 맞춰지기 때문에 merge를 할 수가 있다. 일단은 보편적인 3way merge로 합병 연습을 할 것 참고(기술 면접이나 알고있다고 어필하면 좋은 것) MSA 마이크로 서비스 아키텍쳐 여러 개의 작은 서비스로 구성되고 각 서비스가 독립적으로 개발되고 배포되는 구조 장점 서비스 간 독립성으로 확장성과 유연성이 올라간다 기능 독립성이라는 특징 때문에 일부 서비스가 실패해도 전체 시스템에 영향을 미치지 않는다 단점 서비스간 통신이 필요, 서로 간의 연결 구축이니 관리의 복잡성이 증가 초기 개발 및 통신 등에 시간이 소모 출처: 모찌vlog 파일의 위치를 표시하는 법 절대경로법 최초의 시작점으로 경유한 경로를 전부 기입하는 방식 상대경로법 현재 디렉토리(비교 대상)를 기준으로 작성된 경로를 의미 /. : 내가있는곳 /.. : 내가 있는 곳 상위 디렉터리 #스테이지의 현재상태 확인 git status → git을 시작했는데 등록을 안했다고 오류가 뜨는 경우 → git config 참고 로그와 출력의 차이 (로깅을 해야하는 이유) 로그는 출력한 것을 파일로 뽑는 것이 가능하다. → 추후 elasticsearch와 같은 툴로 활용가능하다. git log 현재 head위치와 commit message를 보내는 시간이나 정보가 나오게 된다. debug, info, error ,fatal → fatal은 시스템에 치명적인 오류 github 블로그를 만드는데 히스토리 관련오류가 발생 JDBC(Java Database Connectivity) API → 우리가 프로그램을 통해 소통할 수 있는 인터페이스 API의 일종으로 Java가 제공하는, Java프로그램 내에서 Database로 접근할 수 있도록 해주는 표준 인터페이스를 JDBC라고 한다. 구체클래스 concrete class 드라이버 → 연결되어 result set이라는 형태의 타입이 존재하는데 애플리케이션에서 사용가능 할 수 있도록 반환해준다. 요즘은 JDBC를 기반으로 하는 툴, JDBC 기술을 가지는 툴을 반환한다. try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } //기본적인 JDBC연결 static void main() { final String PORT = "3306"; final String URL = "jdbc://mysql://localhost:%s/jdbc_shop".formatted(PORT); final String USERNAME = "rapa_jdbc"; final String PASSWORD = "pwd1!"; try (Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD)){ System.out.println("데이터베이스 연결 성공"); System.out.println("사용된 드라이버 이름: " + connection.getMetaData().getDriverName()); System.out.println("연결된 url:" + connection.getMetaData().getURL()); // connection.close(); } catch (SQLException e) { throw new RuntimeException(e); } } 하지만 여러 데이터베이스를 다루고 다른 메인 코드에서 손쉽게 접속하기 위해서는 클래스 화가 필요한데 이때 추상메서드로 지정해서 가져오는 것이 좋다. 다른 코드에서 조작하지 못하게 static final로 변수를 지정해주자. public abstract class ConnectionUtil { public static abstract class H2{ private static final String URL = "jdbc.h2:mem:test_db"; private static final String USER = "sa"; private static final String PASSWORD = ""; public static Connection getConnection(){ try{ return DriverManager.getConnection(URL, USER, PASSWORD); }catch (Exception e){ System.out.println("데이터베이스와의 연결에 실패하였습니다."); throw new RuntimeException(e); } } } public static abstract class MySQL{ private static final String URL = "jdbc.mysql://localhost:3306/jdbc_shop"; private static final String USER = "rapa_jdbc"; private static final String PASSWORD = "pwd1!"; public static Connection getConnection(){ try{ return DriverManager.getConnection(URL, USER, PASSWORD); }catch (Exception e){ System.out.println("데이터베이스와의 연결에 실패하였습니다."); throw new RuntimeException(e); } } } }
Today I Learned
· 2026-04-15
Example Post: thumbnail exists
Follow the guidance Fill the image path to the ‘thumbnail’ attribute, if you want a main image to be displayed on the post header image. --- title: "Example Post: thumbnail exists" date: "2023-12-02" thumbnail: "/assets/img/thumbnail/bricks.webp" --- The rest content is just meaningless dummy text. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Today I Learned
· 2023-12-02
<
>
Touch background to close